Migrate core UI areas to Angular Material
This commit is contained in:
parent
dc45a5581b
commit
d6e3a2a534
|
|
@ -9,15 +9,17 @@
|
|||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^21.0.4",
|
||||
"@angular/cdk": "^21.0.3",
|
||||
"@angular/cdk": "^21.0.5",
|
||||
"@angular/common": "^21.0.4",
|
||||
"@angular/compiler": "^21.0.4",
|
||||
"@angular/core": "^21.0.4",
|
||||
"@angular/elements": "^21.0.8",
|
||||
"@angular/forms": "^21.0.4",
|
||||
"@angular/material": "^21.0.5",
|
||||
"@angular/platform-browser": "^21.0.4",
|
||||
"@angular/platform-browser-dynamic": "^21.0.4",
|
||||
"@angular/router": "^21.0.0",
|
||||
"@fontsource/material-icons-outlined": "^5.2.6",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
|
@ -467,9 +469,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@angular/cdk": {
|
||||
"version": "21.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.0.3.tgz",
|
||||
"integrity": "sha512-abfckeZfFvovdpxuQHRE4gS1VLNa05Dx0ZSKLGVL9DsQsi4pgn6wWg1y9TkXMlmtpG/EhLmCBxUc6LOHfdeWQA==",
|
||||
"version": "21.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.0.5.tgz",
|
||||
"integrity": "sha512-yO/IRYEZ5wJkpwg3GT3b6RST4pqNFTAhuyPdEdLcE81cs283K3aKOsCYh2xUR3bR4WxBh2kBPSJ31AFZyJXbSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse5": "^8.0.0",
|
||||
|
|
@ -638,31 +640,21 @@
|
|||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/localize": {
|
||||
"version": "21.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-21.0.6.tgz",
|
||||
"integrity": "sha512-t4Jf4px49YAuF7G0eLxknB+KNE+tdqzhAo2w//9kjZGg/xHPp9H/RQUlNUB/RYJuabbBh4IpiRvpJZqp7rKt8A==",
|
||||
"dev": true,
|
||||
"node_modules/@angular/material": {
|
||||
"version": "21.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@angular/material/-/material-21.0.5.tgz",
|
||||
"integrity": "sha512-LcDotwwTl2GFtFK1BqcKjj0eRAkOjTSj1C6ux0rOlpU2hJaECxue/ZqEyB/KMHngFAqMJpecxF4zGy/ISCp2xA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "7.28.4",
|
||||
"@types/babel__core": "7.20.5",
|
||||
"tinyglobby": "^0.2.12",
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"localize-extract": "tools/bundles/src/extract/cli.js",
|
||||
"localize-migrate": "tools/bundles/src/migrate/cli.js",
|
||||
"localize-translate": "tools/bundles/src/translate/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "21.0.6",
|
||||
"@angular/compiler-cli": "21.0.6"
|
||||
"@angular/cdk": "21.0.5",
|
||||
"@angular/common": "^21.0.0 || ^22.0.0",
|
||||
"@angular/core": "^21.0.0 || ^22.0.0",
|
||||
"@angular/forms": "^21.0.0 || ^22.0.0",
|
||||
"@angular/platform-browser": "^21.0.0 || ^22.0.0",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
|
|
@ -1694,6 +1686,15 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/material-icons-outlined": {
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/material-icons-outlined/-/material-icons-outlined-5.2.6.tgz",
|
||||
"integrity": "sha512-99XKAkwnCg0s0/ywax+o3m01HSNM5gGzSBw+WnlWG2+WY3wOjcN+wXMfm4zP37Yme7Yze2DvKxF78tHWOrlwFw==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/ansi": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
|
||||
|
|
@ -3356,17 +3357,6 @@
|
|||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-beta.47",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz",
|
||||
|
|
@ -4021,6 +4011,7 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
|
|
@ -4355,59 +4346,6 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7",
|
||||
"@types/babel__generator": "*",
|
||||
"@types/babel__template": "*",
|
||||
"@types/babel__traverse": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__generator": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
||||
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__template": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
||||
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.1.0",
|
||||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__traverse": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
||||
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
|
|
@ -10253,13 +10191,6 @@
|
|||
"peerDependencies": {
|
||||
"zod": "^3.25 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/zone.js": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz",
|
||||
"integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,15 +24,17 @@
|
|||
"packageManager": "npm@10.8.2",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^21.0.4",
|
||||
"@angular/cdk": "^21.0.3",
|
||||
"@angular/cdk": "^21.0.5",
|
||||
"@angular/common": "^21.0.4",
|
||||
"@angular/compiler": "^21.0.4",
|
||||
"@angular/core": "^21.0.4",
|
||||
"@angular/elements": "^21.0.8",
|
||||
"@angular/forms": "^21.0.4",
|
||||
"@angular/material": "^21.0.5",
|
||||
"@angular/platform-browser": "^21.0.4",
|
||||
"@angular/platform-browser-dynamic": "^21.0.4",
|
||||
"@angular/router": "^21.0.0",
|
||||
"@fontsource/material-icons-outlined": "^5.2.6",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { authTokenInterceptor } from './interceptors/auth-token.interceptor';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
|
@ -8,6 +9,7 @@ import { routes } from './app.routes';
|
|||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideAnimations(),
|
||||
provideHttpClient(withInterceptors([authTokenInterceptor])),
|
||||
provideRouter(routes)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.app-topbar {
|
||||
height: 56px !important;
|
||||
min-height: 56px !important;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.logo {
|
||||
height: 40px;
|
||||
|
|
@ -18,15 +24,44 @@
|
|||
min-height: 0; /* necessario per overflow-auto su flex */
|
||||
}
|
||||
|
||||
/* Tabs sticky */
|
||||
.nav-tabs {
|
||||
border-bottom: 1px solid #ccc;
|
||||
.app-tabbar-wrap {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
z-index: 40;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
background: #d8f4e7;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.28);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: #ffffff;
|
||||
color: #0f766e; /* verde elegante */
|
||||
border: 1px solid #d1d5db;
|
||||
border-bottom-color: white;
|
||||
box-shadow: 0 -1px 6px rgba(0, 0, 0, 0.05);
|
||||
.app-tabbar {
|
||||
min-height: 48px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.app-tab-link {
|
||||
min-width: 120px;
|
||||
min-height: 48px;
|
||||
border-radius: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
padding-top: 112px;
|
||||
}
|
||||
|
||||
.app-main-content {
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
margin-top: 0;
|
||||
padding: 10px 16px 16px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,50 @@
|
|||
<div class="main-layout h-screen flex flex-col">
|
||||
|
||||
<!-- Header fisso -->
|
||||
<header
|
||||
class="app-header fixed top-0 left-0 right-0 z-50 flex justify-between items-center px-4 h-16 bg-[#d8f4e7] border-b-none">
|
||||
<div class="left-section flex items-center gap-4">
|
||||
<img src="/logoNoName.png" alt="Logo" class="logo h-10" />
|
||||
<h1 class="app-title text-lg font-medium text-gray-800 m-0">HumAIn Flow</h1>
|
||||
</div>
|
||||
|
||||
<div class="user-section flex items-center gap-2 text-gray-700 font-medium">
|
||||
<i class="bi bi-person-circle fs-4"></i>
|
||||
<span class="username">{{ loggedUser()?.username }}</span>
|
||||
<div class="relative group inline-flex">
|
||||
<i class="bi bi-box-arrow-right
|
||||
text-secondary hover:text-red-600
|
||||
cursor-pointer transition" (click)="logout()" aria-label="Logout" role="button" tabindex="-1">
|
||||
</i>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2
|
||||
opacity-0 group-hover:opacity-100
|
||||
pointer-events-none
|
||||
whitespace-nowrap
|
||||
rounded-md bg-gray-800 px-3 py-1.5
|
||||
text-xs text-white shadow-lg
|
||||
transition-opacity duration-200">
|
||||
Logout
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Wrapper per compensare header fisso -->
|
||||
<div class="pt-16 flex flex-col flex-1 h-[calc(100vh-64px)] overflow-hidden">
|
||||
|
||||
<ul class="nav fixed w-full top-16 z-40
|
||||
flex gap-1 px-3
|
||||
bg-[#d8f4e7] border-b-none shadow-sm h-10">
|
||||
|
||||
<li class="nav-item">
|
||||
<a routerLink="/editor" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }" tabindex="-1" class="nav-link
|
||||
px-4 py-2 rounded-t-md
|
||||
text-gray-700 font-medium
|
||||
transition-all duration-200
|
||||
hover:bg-white/60">
|
||||
<i class="bi bi-diagram-3 me-2"></i>
|
||||
Editor
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a routerLink="/tasks" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }" tabindex="-1" class="nav-link
|
||||
px-4 py-2 rounded-t-md
|
||||
text-gray-700 font-medium
|
||||
transition-all duration-200
|
||||
hover:bg-white/60">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
Tasks
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<!-- Contenuto principale scrollabile -->
|
||||
<main class="main-content w-full h-[calc(100vh-104px)] mt-12 overflow-hidden ml-2 bg-white pb-4 pr-4 rounded-md shadow-sm">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<div class="main-layout flex h-screen flex-col">
|
||||
<mat-toolbar class="app-topbar !fixed !left-0 !right-0 !top-0 !z-50 !bg-[#d8f4e7] !text-slate-800">
|
||||
<div class="flex items-center gap-4">
|
||||
<img src="/logoNoName.png" alt="Logo" class="h-10" />
|
||||
<h1 class="m-0 text-lg font-medium">HumAIn Flow</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="flex-1"></span>
|
||||
|
||||
<div class="flex items-center gap-3 text-sm font-medium text-gray-700">
|
||||
<mat-icon fontIcon="account_circle"></mat-icon>
|
||||
<span class="username">{{ loggedUser()?.username }}</span>
|
||||
<button mat-stroked-button type="button" (click)="logout()">Logout</button>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
|
||||
<div class="app-shell">
|
||||
<div class="app-tabbar-wrap">
|
||||
<nav mat-tab-nav-bar [tabPanel]="tabPanel" class="app-tabbar">
|
||||
<a
|
||||
mat-tab-link
|
||||
routerLink="/editor"
|
||||
routerLinkActive
|
||||
#editorRla="routerLinkActive"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="app-tab-link"
|
||||
[active]="editorRla.isActive">
|
||||
<mat-icon fontIcon="schema"></mat-icon>
|
||||
Editor
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
routerLink="/tasks"
|
||||
routerLinkActive
|
||||
#tasksRla="routerLinkActive"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="app-tab-link"
|
||||
[active]="tasksRla.isActive">
|
||||
<mat-icon fontIcon="task_alt"></mat-icon>
|
||||
Tasks
|
||||
</a>
|
||||
</nav>
|
||||
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
|
||||
</div>
|
||||
|
||||
<main class="main-content app-main-content">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { Component, inject } from '@angular/core';
|
||||
import { afterNextRender, Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
|
||||
@Component({
|
||||
selector: 'app-app-layout',
|
||||
imports: [RouterOutlet, RouterLink, RouterLinkActive],
|
||||
imports: [RouterOutlet, RouterLink, RouterLinkActive, MatButtonModule, MatIconModule, MatTabsModule, MatToolbarModule],
|
||||
templateUrl: './app-layout.html',
|
||||
styleUrl: './app-layout.css',
|
||||
})
|
||||
|
|
@ -13,8 +18,17 @@ export class AppLayout {
|
|||
router = inject(Router);
|
||||
|
||||
private authService = inject(Authorization);
|
||||
private blocksService = inject(BlocksService);
|
||||
|
||||
loggedUser = this.authService.loggedInUser;
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
void this.blocksService.getAllBlocksTypes().catch((err) => {
|
||||
console.error('Blocks preload failed', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authService.logout();
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@
|
|||
<!-- CENTER EDITOR -->
|
||||
<main class="flow-editor-main">
|
||||
@if (flow()) {
|
||||
<div class="flow-editor-canvas-shell">
|
||||
<mat-card class="flow-editor-canvas-shell">
|
||||
<app-title-toolbar></app-title-toolbar>
|
||||
<div class="flow-editor-canvas">
|
||||
<app-rete-editor [flowId]="flow()!.id" [flowData]="flow()!.data"></app-rete-editor>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
} @else {
|
||||
<div class="flow-editor-empty">
|
||||
<h2>No flow selected</h2>
|
||||
|
|
@ -31,9 +31,10 @@
|
|||
<button
|
||||
type="button"
|
||||
class="flow-editor-assistant-toggle"
|
||||
mat-stroked-button
|
||||
(click)="toggleAssistant()"
|
||||
[attr.aria-label]="assistantOpen() ? 'Collapse assistant' : 'Expand assistant'">
|
||||
<i class="bi" [class.bi-layout-sidebar-inset-reverse]="assistantOpen()" [class.bi-stars]="!assistantOpen()"></i>
|
||||
<mat-icon [fontIcon]="assistantOpen() ? 'right_panel_close' : 'auto_awesome'"></mat-icon>
|
||||
<span>{{ assistantOpen() ? 'Hide' : 'AI' }}</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -41,9 +42,9 @@
|
|||
<app-flow-assistant></app-flow-assistant>
|
||||
} @else {
|
||||
<div class="flow-editor-assistant-rail">
|
||||
<i class="bi bi-stars flow-editor-assistant-rail-icon" aria-hidden="true"></i>
|
||||
<mat-icon class="flow-editor-assistant-rail-icon" fontIcon="auto_awesome" aria-hidden="true"></mat-icon>
|
||||
<div class="flow-editor-assistant-rail-text">AI</div>
|
||||
<i class="bi bi-magic flow-editor-assistant-rail-wand" aria-hidden="true"></i>
|
||||
<mat-icon class="flow-editor-assistant-rail-wand" fontIcon="magic_button" aria-hidden="true"></mat-icon>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { Component, inject, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { environment } from '@environment';
|
||||
import { EditorSidebar } from "@pages/main/editor-sidebar/editor-sidebar";
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
|
|
@ -8,7 +11,7 @@ import { ReteEditor } from "@shared/rete-editor/rete-editor";
|
|||
|
||||
@Component({
|
||||
selector: 'app-flow-editor',
|
||||
imports: [EditorSidebar, TitleToolbar, ReteEditor, FlowAssistant],
|
||||
imports: [EditorSidebar, TitleToolbar, ReteEditor, FlowAssistant, MatButtonModule, MatCardModule, MatIconModule],
|
||||
templateUrl: './flow-editor.html',
|
||||
styleUrl: './flow-editor.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<div class="flex flex-col flex-1 h-full">
|
||||
<div class="flex flex-row flex-1 overflow-hidden gap-2">
|
||||
<app-tasks-executions-list
|
||||
class="w-[320px] shrink-0"
|
||||
class="w-[360px] shrink-0"
|
||||
[executions]="executions()"
|
||||
[selectedExecutionId]="selectedExecutionId()"
|
||||
(executionSelected)="selectExecution($event)"
|
||||
(executionDeleteRequested)="removeExecution($event)">
|
||||
</app-tasks-executions-list>
|
||||
|
||||
<main class="flex flex-col w-full bg-gray-100 overflow-hidden rounded-md">
|
||||
<mat-card class="flex w-full flex-col overflow-hidden !rounded-md !bg-gray-100">
|
||||
<app-task-execution-viewer [execution]="selectedExecution()"></app-task-execution-viewer>
|
||||
</main>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, computed, effect, inject, signal } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { normalizeExecutionStatus, TaskExecution } from '@models/task-execution';
|
||||
import {
|
||||
TaskExecutionListItem,
|
||||
|
|
@ -10,7 +11,7 @@ import { TaskExecutionsService } from '@services/task-executions/task-executions
|
|||
|
||||
@Component({
|
||||
selector: 'app-tasks-executor',
|
||||
imports: [TasksExecutionsListComponent, TaskExecutionViewerComponent],
|
||||
imports: [TasksExecutionsListComponent, TaskExecutionViewerComponent, MatCardModule],
|
||||
templateUrl: './tasks-executor.html',
|
||||
styleUrl: './tasks-executor.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,3 +3,99 @@
|
|||
transform: translateY(20px);
|
||||
transition: opacity 500ms ease-out, transform 500ms ease-out;
|
||||
}
|
||||
|
||||
.auth-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
margin: 0 auto;
|
||||
padding: 28px;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
|
||||
.auth-logo-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
width: 12rem;
|
||||
height: 12rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.auth-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.auth-field,
|
||||
.auth-submit {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
margin-left: 4px;
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.auth-banner {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: 32rem;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid;
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
|
||||
.auth-banner-error {
|
||||
border-color: #fca5a5;
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.auth-banner-success {
|
||||
border-color: #86efac;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.auth-banner-icon {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.auth-banner-body {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.auth-banner-body p {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,83 +1,57 @@
|
|||
@if (error()) {
|
||||
<div class="fixed top-4 left-1/2 -translate-x-1/2 z-50
|
||||
w-full max-w-md
|
||||
rounded-lg border border-red-300
|
||||
bg-red-100 text-red-800
|
||||
shadow-md px-4 py-3
|
||||
flex items-start gap-3">
|
||||
|
||||
<!-- Icon -->
|
||||
<i class="bi bi-exclamation-triangle-fill text-red-600 mt-0.5"></i>
|
||||
|
||||
<!-- Message -->
|
||||
<div class="flex-1 text-sm leading-relaxed">
|
||||
<p class="m-0">
|
||||
{{ error() }}
|
||||
</p>
|
||||
<div class="auth-banner auth-banner-error">
|
||||
<mat-icon class="auth-banner-icon" fontIcon="warning"></mat-icon>
|
||||
<div class="auth-banner-body">
|
||||
<p>{{ error() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (registeredUser()) {
|
||||
<div class="fixed top-4 left-1/2 -translate-x-1/2 z-50
|
||||
w-full max-w-md
|
||||
rounded-lg border border-green-300
|
||||
bg-green-100 text-green-800
|
||||
shadow-md px-4 py-3
|
||||
flex items-start gap-3">
|
||||
<!-- Icon -->
|
||||
<i class="bi bi-check-circle-fill text-green-600 mt-0.5"></i>
|
||||
<!-- Message -->
|
||||
<div class="flex-1 text-sm leading-relaxed">
|
||||
<p class="m-0">
|
||||
<div class="auth-banner auth-banner-success">
|
||||
<mat-icon class="auth-banner-icon" fontIcon="check_circle"></mat-icon>
|
||||
<div class="auth-banner-body">
|
||||
<p>
|
||||
Registration successful. <b>{{ registeredUser() }}</b>, now you can log in.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="flex justify-center items-center min-h-screen bg-base-200 p-4">
|
||||
<div class="border-2 border-sky-500/50 w-full max-w-md mx-auto p-7 shadow-xl rounded-md">
|
||||
<div class="flex justify-center">
|
||||
<img class="size-48 " alt="" src="/logoNoName.png" />
|
||||
<div class="auth-page">
|
||||
<mat-card class="auth-card">
|
||||
<div class="auth-logo-wrap">
|
||||
<img class="auth-logo" alt="" src="/logoNoName.png" />
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="auth-card-body">
|
||||
|
||||
<form (ngSubmit)="onSubmit()" novalidate>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="bi bi-at"></i></span>
|
||||
<input type="text" id="username" class="form-control" placeholder="Username"
|
||||
[field]="loginForm.username" />
|
||||
</div>
|
||||
<!-- Password -->
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="password" #password id="password" class="form-control" placeholder="Password"
|
||||
[field]="loginForm.password" />
|
||||
<button class="input-group-text bg-white" type="button" tabindex="-1"
|
||||
<mat-form-field appearance="outline" class="auth-field">
|
||||
<mat-label>Username</mat-label>
|
||||
<input matInput type="text" id="username" placeholder="Username" [field]="loginForm.username" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="auth-field">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput type="password" #password id="password" placeholder="Password" [field]="loginForm.password" />
|
||||
<button mat-button matSuffix type="button" tabindex="-1"
|
||||
(click)="password.type = password.type === 'password' ? 'text' : 'password'"
|
||||
[disabled]="loginFormModel().password.length === 0">
|
||||
<i
|
||||
class="bi"
|
||||
[class.bi-eye]="password.type === 'password'"
|
||||
[class.bi-eye-slash]="password.type === 'text'"></i>
|
||||
{{ password.type === 'password' ? 'Show' : 'Hide' }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Sign up -->
|
||||
<div class="mb-3 text-center">
|
||||
<div class="auth-links">
|
||||
<span>
|
||||
Don't have an account yet?
|
||||
<a routerLink="/sign-up" class="ms-1 link-primary">Sign Up</a>
|
||||
<a routerLink="/sign-up" class="auth-link">Sign Up</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button type="submit" class="btn btn-outline-primary w-100" [disabled]="loginForm().invalid()">
|
||||
<button type="submit" mat-flat-button class="auth-submit" [disabled]="loginForm().invalid()">
|
||||
Login
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { Component, effect, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
import { FormUtility } from '@utilities/form-utility';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
|
|
@ -8,7 +13,7 @@ import { Field, form, required } from '@angular/forms/signals';
|
|||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
imports: [FormsModule, RouterLink, Field],
|
||||
imports: [FormsModule, RouterLink, Field, MatButtonModule, MatCardModule, MatFormFieldModule, MatIconModule, MatInputModule],
|
||||
templateUrl: './login.html',
|
||||
styleUrls: ['./login.css'],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
.signup-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.signup-card {
|
||||
width: 100%;
|
||||
max-width: 48rem;
|
||||
padding: 28px;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
|
||||
.signup-logo-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.signup-logo {
|
||||
width: 12rem;
|
||||
height: 12rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.signup-card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.signup-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.signup-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.signup-field,
|
||||
.signup-submit {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.signup-links {
|
||||
margin-bottom: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signup-link {
|
||||
margin-left: 4px;
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.signup-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.signup-error {
|
||||
padding: 16px;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 10px;
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.signup-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +1,88 @@
|
|||
<div class="flex justify-center items-center min-h-screen bg-base-400 p-4">
|
||||
|
||||
<div class="border-2 border-sky-500/50 w-3/5 min-w-100 mx-auto p-7 shadow-xl rounded-md">
|
||||
<div class="flex justify-center">
|
||||
<img class="size-48 " alt="" src="/logoNoName.png" />
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
|
||||
<form (ngSubmit)="onSubmit()" class="flex flex-col gap-3">
|
||||
<div class="mb-3">
|
||||
<label for="fullname" class="form-label">Full Name</label>
|
||||
<input id="fullname" type="text" class="form-control"
|
||||
[class.is-invalid]="isInvalid(signupForm.fullname())" [field]="signupForm.fullname"
|
||||
placeholder="Paolo Rossi" />
|
||||
|
||||
@if (isInvalid(signupForm.fullname())) {
|
||||
<small class="invalid-feedback">{{ signupForm.fullname().errors()[0].message }}</small>
|
||||
}
|
||||
</div>
|
||||
<div class="flex lg:flex-row flex-col gap-3">
|
||||
<div class="mb-3 w-full">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input id="email" type="email" class="form-control"
|
||||
[class.is-invalid]="isInvalid(signupForm.email())" [field]="signupForm.email"
|
||||
placeholder="email@example.com" />
|
||||
|
||||
@if (isInvalid(signupForm.email())) {
|
||||
<small class="invalid-feedback">{{ signupForm.email().errors()[0].message }}</small>
|
||||
}
|
||||
</div>
|
||||
<div class="mb-3 w-full">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input id="username" type="text" class="form-control"
|
||||
[class.is-invalid]="isInvalid(signupForm.username())" [field]="signupForm.username"
|
||||
placeholder="Username" />
|
||||
@if (isInvalid(signupForm.username())) {
|
||||
<small class="invalid-feedback">{{ signupForm.username().errors()[0].message }}</small>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex lg:flex-row flex-col gap-3">
|
||||
<div class="mb-3 w-full">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<div class="input-group">
|
||||
<input id="password" type="password" class="form-control" #password
|
||||
[class.is-invalid]="isInvalid(signupForm.password())" [field]="signupForm.password"
|
||||
placeholder="********" />
|
||||
<button class="input-group-text bg-white" type="button" tabindex="-1"
|
||||
(click)="password.type = password.type === 'password' ? 'text' : 'password'"
|
||||
[disabled]="signupModel().password.length === 0">
|
||||
<i
|
||||
class="bi"
|
||||
[class.bi-eye]="password.type === 'password'"
|
||||
[class.bi-eye-slash]="password.type === 'text'"></i>
|
||||
</button>
|
||||
@if (isInvalid(signupForm.password())) {
|
||||
<small class="invalid-feedback">{{ signupForm.password().errors()[0].message }}</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="mb-3 w-full">
|
||||
<label for="confirmPassword" class="form-label">Confirm Password</label>
|
||||
<div class="input-group">
|
||||
<input id="confirmPassword" type="password" class="form-control" #confirmPassword
|
||||
[class.is-invalid]="!isInvalid(signupForm.password()) && isInvalid(signupForm.confirmPassword())"
|
||||
[field]="signupForm.confirmPassword" placeholder="********" />
|
||||
<button class="input-group-text bg-white" type="button" tabindex="-1"
|
||||
(click)="confirmPassword.type = confirmPassword.type === 'password' ? 'text' : 'password'"
|
||||
[disabled]="signupModel().confirmPassword.length === 0">
|
||||
<i
|
||||
class="bi"
|
||||
[class.bi-eye]="confirmPassword.type === 'password'"
|
||||
[class.bi-eye-slash]="confirmPassword.type === 'text'"></i>
|
||||
</button>
|
||||
@if (!isInvalid(signupForm.password()) && isInvalid(signupForm.confirmPassword())) {
|
||||
<small class="invalid-feedback">{{
|
||||
signupForm.confirmPassword().errors()[0].message}}</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 text-center">
|
||||
<span>
|
||||
Already have an account?
|
||||
<a routerLink="/sign-in" class="ms-1 link-primary">Sign In</a>
|
||||
</span>
|
||||
</div>
|
||||
@if(error()) {
|
||||
<div class="mt-2 bg-red-100 border border-red-200 text-sm text-red-800 rounded-lg p-4 dark:bg-red-800/10 dark:border-red-900 dark:text-red-500"
|
||||
role="alert" tabindex="-1" aria-labelledby="hs-soft-color-danger-label">
|
||||
<span id="hs-soft-color-danger-label" class="font-bold">Error</span> {{error()}}
|
||||
</div>
|
||||
} @else {
|
||||
@if (!isRegistering()){
|
||||
<!-- Submit -->
|
||||
<button type="submit" class="btn btn-outline-primary w-full p-2" [disabled]="signupForm().invalid()">
|
||||
Register
|
||||
</button>
|
||||
} @else {
|
||||
<button type="button" class="btn btn-outline-primary w-full p-2" disabled>
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
Registering...
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
<div class="signup-page">
|
||||
<mat-card class="signup-card">
|
||||
<div class="signup-logo-wrap">
|
||||
<img class="signup-logo" alt="" src="/logoNoName.png" />
|
||||
</div>
|
||||
|
||||
<div class="signup-card-body">
|
||||
<form (ngSubmit)="onSubmit()" class="signup-form">
|
||||
<mat-form-field appearance="outline" class="signup-field">
|
||||
<mat-label>Full Name</mat-label>
|
||||
<input matInput id="fullname" type="text" [field]="signupForm.fullname" placeholder="Paolo Rossi" />
|
||||
@if (isInvalid(signupForm.fullname())) {
|
||||
<mat-error>{{ signupForm.fullname().errors()[0].message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div class="signup-row">
|
||||
<mat-form-field appearance="outline" class="signup-field">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput id="email" type="email" [field]="signupForm.email" placeholder="email@example.com" />
|
||||
@if (isInvalid(signupForm.email())) {
|
||||
<mat-error>{{ signupForm.email().errors()[0].message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="signup-field">
|
||||
<mat-label>Username</mat-label>
|
||||
<input matInput id="username" type="text" [field]="signupForm.username" placeholder="Username" />
|
||||
@if (isInvalid(signupForm.username())) {
|
||||
<mat-error>{{ signupForm.username().errors()[0].message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="signup-row">
|
||||
<mat-form-field appearance="outline" class="signup-field">
|
||||
<mat-label>Password</mat-label>
|
||||
<input matInput id="password" type="password" #password [field]="signupForm.password" placeholder="********" />
|
||||
<button mat-button matSuffix type="button" tabindex="-1"
|
||||
(click)="password.type = password.type === 'password' ? 'text' : 'password'"
|
||||
[disabled]="signupModel().password.length === 0">
|
||||
{{ password.type === 'password' ? 'Show' : 'Hide' }}
|
||||
</button>
|
||||
@if (isInvalid(signupForm.password())) {
|
||||
<mat-error>{{ signupForm.password().errors()[0].message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="signup-field">
|
||||
<mat-label>Confirm Password</mat-label>
|
||||
<input matInput id="confirmPassword" type="password" #confirmPassword [field]="signupForm.confirmPassword" placeholder="********" />
|
||||
<button mat-button matSuffix type="button" tabindex="-1"
|
||||
(click)="confirmPassword.type = confirmPassword.type === 'password' ? 'text' : 'password'"
|
||||
[disabled]="signupModel().confirmPassword.length === 0">
|
||||
{{ confirmPassword.type === 'password' ? 'Show' : 'Hide' }}
|
||||
</button>
|
||||
@if (!isInvalid(signupForm.password()) && isInvalid(signupForm.confirmPassword())) {
|
||||
<mat-error>{{ signupForm.confirmPassword().errors()[0].message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="signup-links">
|
||||
<span>
|
||||
Already have an account?
|
||||
<a routerLink="/sign-in" class="signup-link">Sign In</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if(error()) {
|
||||
<div class="signup-error" role="alert">
|
||||
<span class="font-bold">Error</span> {{error()}}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isRegistering()) {
|
||||
<button type="submit" mat-flat-button class="signup-submit" [disabled]="signupForm().invalid()">
|
||||
Register
|
||||
</button>
|
||||
} @else {
|
||||
<button type="button" mat-flat-button class="signup-submit" disabled>
|
||||
Registering...
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { Component, effect, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { form, Field, minLength, email, required, validate, maxLength, disabled } from '@angular/forms/signals'
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { UserRegistration } from '@models/user';
|
||||
|
|
@ -9,7 +13,7 @@ import { FormUtility } from '@utilities/form-utility';
|
|||
|
||||
@Component({
|
||||
selector: 'app-signup',
|
||||
imports: [FormsModule, Field, RouterLink],
|
||||
imports: [FormsModule, Field, RouterLink, MatButtonModule, MatCardModule, MatFormFieldModule, MatInputModule],
|
||||
templateUrl: './signup.html',
|
||||
styleUrl: './signup.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,14 @@
|
|||
.editor-sidebar-shell {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.editor-sidebar-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.enabled:hover {
|
||||
background-color: var(--color-gray-100) ;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<div
|
||||
class="transition-all duration-300 ease-in-out overflow-hidden h-[calc(100vh-104px)]"
|
||||
class="editor-sidebar-shell transition-all duration-300 ease-in-out overflow-hidden"
|
||||
[class.w-14]="collapsed()"
|
||||
[class.w-72]="!collapsed()"
|
||||
[class.w-80]="!collapsed()"
|
||||
>
|
||||
<aside class="h-full w-full min-w-0 bg-white border-r border-gray-200 flex overflow-hidden">
|
||||
<div class="flex flex-col h-full shrink-0">
|
||||
<div class="flex flex-col h-full min-h-0 shrink-0">
|
||||
|
||||
<button (click)="collapse()" class="relative
|
||||
w-6 h-6 rounded-full
|
||||
|
|
@ -28,9 +28,9 @@
|
|||
|
||||
@if (!collapsed()) {
|
||||
<!-- CONTENT -->
|
||||
<div class="h-full min-w-0 bg-white flex-1 overflow-hidden">
|
||||
<div class="h-full min-h-0 min-w-0 bg-white flex-1 overflow-hidden">
|
||||
|
||||
<div class="space-y-6 h-full min-w-0 overflow-hidden">
|
||||
<div class="editor-sidebar-content h-full min-h-0 min-w-0 overflow-hidden">
|
||||
@switch (open) {
|
||||
@case ('flows') {
|
||||
<app-group-holder title="Flows" icon="bi-lightning-charge-fill">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.blocks-list-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.blocks-list-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.blocks-list-search {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.blocks-list-empty {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding-top: 2.5rem;
|
||||
margin-top: 2.5rem;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.blocks-list-items {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 320px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.blocks-list-card {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
border: 1px solid #dbe7f5;
|
||||
border-radius: 16px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(56, 189, 248, 0.12), transparent 34%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
||||
box-shadow:
|
||||
0 12px 26px rgba(15, 23, 42, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
||||
transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.blocks-list-card:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.blocks-list-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: #7dd3fc;
|
||||
box-shadow: 0 18px 30px rgba(14, 116, 144, 0.14);
|
||||
}
|
||||
|
||||
.blocks-list-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 14px 0;
|
||||
}
|
||||
|
||||
.blocks-list-card-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
margin: 0;
|
||||
border-radius: 12px;
|
||||
color: #075985;
|
||||
background: linear-gradient(180deg, #e0f2fe 0%, #bae6fd 100%);
|
||||
}
|
||||
|
||||
.blocks-list-card-avatar .mat-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.blocks-list-card-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.blocks-list-card-subtitle {
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.blocks-list-card-content {
|
||||
padding: 0 14px 8px;
|
||||
}
|
||||
|
||||
.blocks-list-card-copy {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.blocks-list-card-actions {
|
||||
padding: 0 14px 14px !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.blocks-list-card-chip {
|
||||
min-height: 28px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #0369a1 !important;
|
||||
background: rgba(224, 242, 254, 0.95) !important;
|
||||
border: 1px solid rgba(125, 211, 252, 0.7);
|
||||
}
|
||||
|
||||
.blocks-list-card-chip-interactive {
|
||||
color: #b45309 !important;
|
||||
background: rgba(254, 243, 199, 0.98) !important;
|
||||
border-color: rgba(252, 211, 77, 0.72);
|
||||
}
|
||||
|
|
@ -1,24 +1,15 @@
|
|||
@if (loading()) {
|
||||
<div class="flex items-center justify-center h-32">
|
||||
<div class="spinner-border text-indigo-500" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div class="blocks-list-loading">
|
||||
<mat-spinner diameter="32"></mat-spinner>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="h-full flex flex-col w-full min-w-0 overflow-x-hidden">
|
||||
<div class="w-full max-w-sm py-2 h-15">
|
||||
<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 blocks..." [(ngModel)]="searchTerm" />
|
||||
</div>
|
||||
<div class="blocks-list-root">
|
||||
<div class="blocks-list-search">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Search blocks</mat-label>
|
||||
<mat-icon matPrefix fontIcon="search"></mat-icon>
|
||||
<input matInput placeholder="Search blocks..." [(ngModel)]="searchTerm" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
|
|
@ -28,28 +19,34 @@
|
|||
-->
|
||||
|
||||
@if (filteredBlocks().length === 0) {
|
||||
<div class="text-gray-400 text-center mt-10 flex flex-col flex-1 pt-10">
|
||||
<div class="blocks-list-empty">
|
||||
No blocks found
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full min-w-0 h-full py-2 gap-2 flex flex-col items-stretch max-h-[calc(100vh-320px)]">
|
||||
<div class="blocks-list-items">
|
||||
@for (block of filteredBlocks(); track block.type) {
|
||||
<div
|
||||
class="border px-3 py-2 w-full min-w-0 bg-slate-50 border-slate-200 overflow-hidden cursor-grab active:cursor-grabbing"
|
||||
<mat-card
|
||||
class="blocks-list-card"
|
||||
draggable="true"
|
||||
(dragstart)="onDragStart($event, block)">
|
||||
<div class="flex items-center justify-between gap-2 text-sm min-w-0">
|
||||
<span class="font-medium text-slate-700 min-w-0 truncate">{{ block.type }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-1">{{ block.description }}</p>
|
||||
<span class="text-xs px-2 py-0.5 rounded"
|
||||
[class.bg-amber-100]="block.userInteractive"
|
||||
[class.text-amber-700]="block.userInteractive"
|
||||
[class.bg-sky-100]="!block.userInteractive"
|
||||
[class.text-sky-700]="!block.userInteractive">
|
||||
<mat-card-header class="blocks-list-card-header">
|
||||
<div matCardAvatar class="blocks-list-card-avatar">
|
||||
<mat-icon [fontIcon]="block.userInteractive ? 'touch_app' : 'auto_awesome'"></mat-icon>
|
||||
</div>
|
||||
<mat-card-title class="blocks-list-card-title">{{ block.type }}</mat-card-title>
|
||||
<mat-card-subtitle class="blocks-list-card-subtitle">
|
||||
{{ block.userInteractive ? 'Interactive' : 'Automatic' }}
|
||||
</span>
|
||||
</div>
|
||||
</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content class="blocks-list-card-content">
|
||||
<p class="blocks-list-card-copy">{{ block.description }}</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions class="blocks-list-card-actions" align="end">
|
||||
<mat-chip class="blocks-list-card-chip" [class.blocks-list-card-chip-interactive]="block.userInteractive">
|
||||
{{ block.userInteractive ? 'Human step' : 'Automated step' }}
|
||||
</mat-chip>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,17 @@ import { BlockType } from '@models/flow';
|
|||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { ListStateViewHolder, OrderViewState } from '@utilities/list-state-holder';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { BLOCK_TYPE_DRAG_MIME } from './block-drag';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blocks-list',
|
||||
imports: [FormsModule],
|
||||
imports: [FormsModule, MatCardModule, MatChipsModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule],
|
||||
templateUrl: './blocks-list.html',
|
||||
styleUrl: './blocks-list.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,24 +7,22 @@
|
|||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="relative z-10 w-full max-w-md
|
||||
rounded-lg bg-white shadow-xl
|
||||
p-5 animate-fade-in">
|
||||
<div class="relative z-10 w-full max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-2xl">
|
||||
|
||||
<h3 class="text-lg font-semibold mb-3">
|
||||
<h3 class="mb-3 text-lg font-semibold text-slate-900">
|
||||
Confirm
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-gray-700 mb-5">
|
||||
<p class="mb-6 text-sm text-slate-600">
|
||||
{{ this.state()!.message }}
|
||||
</p>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-outline-secondary" (click)="close(false)">
|
||||
<button mat-stroked-button (click)="close(false)">
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary" (click)="close(true)">
|
||||
<button mat-flat-button (click)="close(true)">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -32,4 +30,4 @@
|
|||
|
||||
</div>
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { ConfirmDialogService } from '@services/dialogs/confirm-dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-dialog-host',
|
||||
standalone: true,
|
||||
imports: [MatButtonModule],
|
||||
templateUrl: './confirm-dialog.html',
|
||||
})
|
||||
export class ConfirmDialogHostComponent {
|
||||
|
|
|
|||
|
|
@ -52,16 +52,18 @@
|
|||
</div>
|
||||
|
||||
@if (modelPickerOpen()) {
|
||||
<select
|
||||
id="assistant-model"
|
||||
class="assistant-select"
|
||||
[ngModel]="selectedModel()"
|
||||
(ngModelChange)="selectModel($event)"
|
||||
[disabled]="modelsLoading() || !models().length">
|
||||
@for (model of models(); track model) {
|
||||
<option [value]="model">{{ model }}</option>
|
||||
}
|
||||
</select>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Model</mat-label>
|
||||
<mat-select
|
||||
id="assistant-model"
|
||||
[ngModel]="selectedModel()"
|
||||
(ngModelChange)="selectModel($event)"
|
||||
[disabled]="modelsLoading() || !models().length">
|
||||
@for (model of models(); track model) {
|
||||
<mat-option [value]="model">{{ model }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@if (modelsLoading()) {
|
||||
|
|
@ -87,7 +89,7 @@
|
|||
@if (quickPromptsOpen()) {
|
||||
<div class="assistant-chip-grid">
|
||||
@for (starter of starterPrompts; track starter) {
|
||||
<button type="button" class="assistant-chip" (click)="useStarter(starter)">
|
||||
<button type="button" mat-stroked-button class="assistant-chip" (click)="useStarter(starter)">
|
||||
{{ starter }}
|
||||
</button>
|
||||
}
|
||||
|
|
@ -130,17 +132,22 @@
|
|||
</div>
|
||||
|
||||
<form class="assistant-composer" (ngSubmit)="submitPrompt()">
|
||||
<textarea
|
||||
class="assistant-input"
|
||||
[ngModel]="prompt()"
|
||||
(ngModelChange)="prompt.set($event)"
|
||||
[disabled]="!selectedModel() || assistantBusy()"
|
||||
name="assistantPrompt"
|
||||
rows="4"
|
||||
placeholder="Ask the assistant to create or refine a workflow"></textarea>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Prompt</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
class="assistant-input"
|
||||
[ngModel]="prompt()"
|
||||
(ngModelChange)="prompt.set($event)"
|
||||
[disabled]="!selectedModel() || assistantBusy()"
|
||||
name="assistantPrompt"
|
||||
rows="4"
|
||||
placeholder="Ask the assistant to create or refine a workflow"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
mat-flat-button
|
||||
class="assistant-send"
|
||||
[disabled]="!selectedModel() || assistantBusy() || !prompt().trim()">
|
||||
Send
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { environment } from '@environment';
|
||||
import {
|
||||
AssistantCallPhase,
|
||||
|
|
@ -18,7 +22,7 @@ import { finalize, take } from 'rxjs';
|
|||
|
||||
@Component({
|
||||
selector: 'app-flow-assistant',
|
||||
imports: [CommonModule, FormsModule],
|
||||
imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatSelectModule],
|
||||
templateUrl: './flow-assistant.html',
|
||||
styleUrl: './flow-assistant.css'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
.flow-item-card {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 14px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(59, 130, 246, 0.12), transparent 32%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f3f8ff 100%);
|
||||
box-shadow:
|
||||
0 10px 24px rgba(15, 23, 42, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
||||
transition: transform 0.16s ease, border-color 0.16s ease, background-color 0.16s ease, box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.flow-item-card-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flow-item-card-clickable:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: #3b82f6;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(96, 165, 250, 0.18), transparent 34%),
|
||||
linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
|
||||
box-shadow: 0 18px 30px rgba(37, 99, 235, 0.16);
|
||||
}
|
||||
|
||||
.flow-item-card-open {
|
||||
border-color: #2563eb;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(37, 99, 235, 0.2), transparent 34%),
|
||||
linear-gradient(180deg, #dbeafe 0%, #bfdbfe 100%);
|
||||
box-shadow: 0 20px 34px rgba(37, 99, 235, 0.18);
|
||||
}
|
||||
|
||||
.flow-item-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flow-item-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flow-item-title {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.flow-item-author {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.flow-item-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.flow-item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flow-item-visibility-icon {
|
||||
color: #1e40af !important;
|
||||
background: rgba(219, 234, 254, 0.88) !important;
|
||||
border: 1px solid rgba(96, 165, 250, 0.45);
|
||||
}
|
||||
|
||||
.flow-item-actions .mat-mdc-icon-button.mat-mdc-button-base {
|
||||
color: #475569;
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
}
|
||||
|
||||
.flow-item-actions .mat-mdc-icon-button.mat-mdc-button-base:hover {
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
|
@ -1,40 +1,35 @@
|
|||
<div class="w-full overflow-hidden text-left p-3 rounded-md border transition-all duration-150 select-none hover:-translate-y-0.5 hover:border-blue-300 hover:bg-blue-100 hover:shadow-md"
|
||||
[class.cursor-pointer]="flow().id !== openedFlowId()"
|
||||
[class.cursor-default]="flow().id === openedFlowId()"
|
||||
[class.border-blue-300]="flow().id === openedFlowId()"
|
||||
[class.bg-blue-50]="flow().id === openedFlowId()"
|
||||
[class.border-slate-200]="flow().id !== openedFlowId()"
|
||||
[class.bg-white]="flow().id !== openedFlowId()"
|
||||
<mat-card class="flow-item-card"
|
||||
[class.flow-item-card-clickable]="flow().id !== openedFlowId()"
|
||||
[class.flow-item-card-open]="flow().id === openedFlowId()"
|
||||
(click)="flow().id !== openedFlowId() && open()">
|
||||
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex-1 min-w-0 overflow-hidden">
|
||||
<div class="text-sm font-medium text-slate-800 truncate">{{ flow().name }}</div>
|
||||
<div class="text-xs text-slate-500 truncate">{{ flow().author }}</div>
|
||||
<div class="flow-item-head">
|
||||
<div class="flow-item-meta">
|
||||
<div class="flow-item-title">{{ flow().name }}</div>
|
||||
<div class="flow-item-author">{{ flow().author }}</div>
|
||||
</div>
|
||||
<span class="shrink-0 text-[10px] px-2 py-0.5 rounded-full border font-semibold"
|
||||
[class.bg-indigo-100]="flow().visibility === 'PUBLIC'"
|
||||
[class.text-indigo-700]="flow().visibility === 'PUBLIC'"
|
||||
[class.border-indigo-200]="flow().visibility === 'PUBLIC'"
|
||||
[class.bg-slate-100]="flow().visibility === 'PRIVATE'"
|
||||
[class.text-slate-700]="flow().visibility === 'PRIVATE'"
|
||||
[class.border-slate-200]="flow().visibility === 'PRIVATE'">
|
||||
{{ flow().visibility }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-[11px] text-slate-500 flex justify-between items-center gap-2">
|
||||
<div class="flow-item-footer">
|
||||
<span>{{ flow().createdAt | date:'dd/MM/yyyy' }}</span>
|
||||
<div class="flex items-center gap-1.5 text-sm">
|
||||
<button class="p-1 rounded enabled:hover:bg-blue-100 enabled:text-blue-600 disabled:text-blue-200" title="Clone"
|
||||
(click)="$event.stopPropagation(); clone()" [disabled]="this.flow().visibility!=='PRIVATE'">
|
||||
<i class="bi bi-files"></i>
|
||||
<div class="flow-item-actions">
|
||||
<button
|
||||
mat-icon-button
|
||||
[matTooltip]="flow().visibility === 'PRIVATE' ? 'Private flow' : 'Public flow'"
|
||||
class="flow-item-visibility-icon"
|
||||
(click)="$event.stopPropagation()"
|
||||
disabled>
|
||||
<mat-icon [fontIcon]="flow().visibility === 'PRIVATE' ? 'lock' : 'public'"></mat-icon>
|
||||
</button>
|
||||
<button class="p-1 rounded enabled:hover:bg-red-100 enabled:text-red-600 disabled:text-red-200" title="Delete"
|
||||
<button mat-icon-button matTooltip="Clone flow"
|
||||
(click)="$event.stopPropagation(); clone()" [disabled]="this.flow().visibility!=='PRIVATE'">
|
||||
<mat-icon fontIcon="content_copy"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button matTooltip="Delete flow"
|
||||
(click)="$event.stopPropagation(); remove()" [disabled]="this.flow().visibility!=='PRIVATE'">
|
||||
<i class="bi bi-trash"></i>
|
||||
<mat-icon fontIcon="delete"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</mat-card>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, inject, input, model } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { Flow } from '@models/flow';
|
||||
import { ConfirmDialogService } from '@services/dialogs/confirm-dialog';
|
||||
import { FlowsService } from '@services/flows/flows';
|
||||
|
|
@ -7,7 +11,7 @@ import { EditorStateHolder } from '@stores/flow-editor';
|
|||
|
||||
@Component({
|
||||
selector: 'app-flow-item',
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, MatButtonModule, MatCardModule, MatIconModule, MatTooltipModule],
|
||||
templateUrl: './flow-item.html',
|
||||
styleUrl: './flow-item.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.flows-list-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.flows-list-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.flows-list-search {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.flows-list-filter {
|
||||
display: inline-flex;
|
||||
align-self: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.flows-list-ordering {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 4.5rem;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.flows-list-empty {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding-top: 2.5rem;
|
||||
margin-top: 2.5rem;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flows-list-items {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 380px);
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 0 20px !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.flows-list-list-item {
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
margin-bottom: 10px;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.flows-list-list-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:host ::ng-deep .flows-list-list-item .mdc-list-item__content,
|
||||
:host ::ng-deep .flows-list-list-item .mat-mdc-list-item-unscoped-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
|
@ -1,58 +1,38 @@
|
|||
@if (loading()) {
|
||||
<div class="flex items-center justify-center h-32">
|
||||
<div class="spinner-border text-indigo-500" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div class="flows-list-loading">
|
||||
<mat-spinner diameter="32"></mat-spinner>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="w-full max-w-sm py-2 h-15">
|
||||
<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 class="flows-list-root">
|
||||
<div class="flows-list-search">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Search flows</mat-label>
|
||||
<mat-icon matPrefix fontIcon="search"></mat-icon>
|
||||
<input matInput placeholder="Search flows..." [(ngModel)]="searchTerm" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="btn-group w-full mb-3 h-8" role="group">
|
||||
<mat-button-toggle-group class="flows-list-filter" [value]="filter()" (valueChange)="filter.set($event)">
|
||||
<mat-button-toggle value="all">All</mat-button-toggle>
|
||||
<mat-button-toggle value="PUBLIC">Public</mat-button-toggle>
|
||||
<mat-button-toggle value="PRIVATE">Private</mat-button-toggle>
|
||||
</mat-button-toggle-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>
|
||||
|
||||
<div class="flex py-2 text-sm w-full items-center text-center justify-center h-18">
|
||||
<div class="flows-list-ordering">
|
||||
<app-ordering [orderFields]="orderFields" [orderView]="orderView" (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">
|
||||
<div class="flows-list-empty">
|
||||
No flows found
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex-1 overflow-y-auto w-full h-full py-2 gap-2 flex flex-col items-stretch max-h-[calc(100vh-380px)]">
|
||||
<mat-list class="flows-list-items">
|
||||
@for (flow of filteredFlows(); track flow.id) {
|
||||
<app-flow-item [(detailOpenedId)]="detailOpenedId" [flow]="flow"></app-flow-item>
|
||||
<mat-list-item class="flows-list-list-item">
|
||||
<app-flow-item [(detailOpenedId)]="detailOpenedId" [flow]="flow"></app-flow-item>
|
||||
</mat-list-item>
|
||||
}
|
||||
</div>
|
||||
</mat-list>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@ import { Flow, FlowVisibility } from '@models/flow';
|
|||
import { FlowsService } from '@services/flows/flows';
|
||||
import { FlowItem } from './flow-item/flow-item';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { OrderEvent, OrderField, Ordering } from "@shared/ordering/ordering";
|
||||
import { ListStateViewHolder, OrderViewState } from '@utilities/list-state-holder';
|
||||
|
||||
|
|
@ -10,7 +17,7 @@ type FlowFilter = FlowVisibility | 'all';
|
|||
|
||||
@Component({
|
||||
selector: 'app-flows-list',
|
||||
imports: [FlowItem, FormsModule, Ordering],
|
||||
imports: [FlowItem, FormsModule, Ordering, MatButtonToggleModule, MatCardModule, MatFormFieldModule, MatIconModule, MatInputModule, MatListModule, MatProgressSpinnerModule],
|
||||
templateUrl: './flows-list.html',
|
||||
styleUrl: './flows-list.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
:host {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.group-holder-body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
|
@ -22,7 +22,8 @@
|
|||
<!-- Body -->
|
||||
<div
|
||||
class="transition-all duration-300 px-2 pb-2 flex flex-col flex-1 min-w-0 overflow-hidden">
|
||||
<div class="group-holder-body">
|
||||
<ng-content></ng-content>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<h3 class="text-lg font-semibold text-slate-900">{{ currentState.title }}</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Confirm the input as node output or edit it before sending.</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="cancel($event)">Close</button>
|
||||
<button type="button" mat-stroked-button (click)="cancel($event)">Close</button>
|
||||
</div>
|
||||
|
||||
<div class="grid flex-1 gap-4 overflow-y-auto px-5 py-4">
|
||||
|
|
@ -23,27 +23,28 @@
|
|||
</fieldset>
|
||||
|
||||
@if (editing()) {
|
||||
<label class="block text-sm font-medium text-slate-700">
|
||||
Edit Response
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Edit Response</mat-label>
|
||||
<textarea
|
||||
class="form-control mt-2 min-h-56"
|
||||
matInput
|
||||
class="min-h-56"
|
||||
rows="10"
|
||||
[ngModel]="draftValue"
|
||||
[attr.data-autofocus]="'true'"
|
||||
(ngModelChange)="setDraftValue($event)">
|
||||
</textarea>
|
||||
</label>
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 border-t border-slate-200 px-5 py-4">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel($event)">Cancel</button>
|
||||
<button type="button" mat-stroked-button (click)="cancel($event)">Cancel</button>
|
||||
@if (!editing()) {
|
||||
<button type="button" class="btn btn-outline-primary" (click)="startEditing($event)">Edit Response</button>
|
||||
<button type="button" class="btn btn-primary" (click)="confirmInput($event)">Confirm Input</button>
|
||||
<button type="button" mat-stroked-button (click)="startEditing($event)">Edit Response</button>
|
||||
<button type="button" mat-flat-button (click)="confirmInput($event)">Confirm Input</button>
|
||||
} @else {
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="backToConfirm($event)">Back</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!canSendEditedOutput()" (click)="sendEditedOutput($event)">Send Output</button>
|
||||
<button type="button" mat-stroked-button (click)="backToConfirm($event)">Back</button>
|
||||
<button type="button" mat-flat-button [disabled]="!canSendEditedOutput()" (click)="sendEditedOutput($event)">Send Output</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { Component, effect, ElementRef, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import {
|
||||
HumanInteractionDialogResult,
|
||||
HumanInteractionDialogService
|
||||
|
|
@ -8,7 +11,7 @@ import {
|
|||
@Component({
|
||||
selector: 'app-human-interaction-dialog-host',
|
||||
standalone: true,
|
||||
imports: [FormsModule],
|
||||
imports: [FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule],
|
||||
templateUrl: './human-interaction-dialog.html'
|
||||
})
|
||||
export class HumanInteractionDialogHostComponent {
|
||||
|
|
|
|||
|
|
@ -2,19 +2,15 @@
|
|||
<div class="fixed inset-0 z-9999">
|
||||
<div class="absolute inset-0 bg-black/50" (click)="cancel($event)"></div>
|
||||
|
||||
<div class="absolute inset-6 rounded-xl bg-white shadow-2xl border border-slate-200 p-5 flex flex-col gap-4 overflow-auto">
|
||||
<div class="absolute inset-6 flex flex-col gap-4 overflow-auto rounded-2xl border border-slate-200 bg-white p-6 shadow-2xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-slate-900">{{ state()!.title }}</h3>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="cancel($event)">Close</button>
|
||||
<button type="button" mat-stroked-button (click)="cancel($event)">Close</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4">
|
||||
@for (field of fields; track field.key) {
|
||||
<label class="text-sm font-medium text-slate-700">
|
||||
@if (field.type !== 'display') {
|
||||
{{ field.label }}
|
||||
}
|
||||
|
||||
<div class="text-sm font-medium text-slate-700">
|
||||
@switch (field.type) {
|
||||
@case ('display') {
|
||||
<fieldset class="mt-1 border border-slate-200 rounded-md bg-slate-50 p-3">
|
||||
|
|
@ -23,11 +19,12 @@
|
|||
@if (field.copyable) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary py-0 px-2 text-[11px]"
|
||||
title="Copy"
|
||||
mat-stroked-button
|
||||
class="!min-w-0 !px-2 !py-0 !text-[11px]"
|
||||
matTooltip="Copy"
|
||||
aria-label="Copy value"
|
||||
(click)="copyFieldValue(field.key, $event)">
|
||||
<i class="bi bi-copy"></i>
|
||||
<mat-icon fontIcon="content_copy"></mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
|
@ -35,65 +32,72 @@
|
|||
</fieldset>
|
||||
}
|
||||
@case ('textarea') {
|
||||
<textarea
|
||||
class="form-control mt-1"
|
||||
[rows]="field.rows ?? 8"
|
||||
[placeholder]="field.placeholder ?? ''"
|
||||
[readonly]="field.readonly === true"
|
||||
[attr.autofocus]="field.autofocus ? '' : null"
|
||||
[attr.data-autofocus]="field.autofocus ? 'true' : null"
|
||||
[ngModel]="draft[field.key]"
|
||||
(ngModelChange)="setFieldValue(field.key, $event)">
|
||||
</textarea>
|
||||
<mat-form-field appearance="outline" class="mt-1">
|
||||
<mat-label>{{ field.label }}</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
[rows]="field.rows ?? 8"
|
||||
[placeholder]="field.placeholder ?? ''"
|
||||
[readonly]="field.readonly === true"
|
||||
[attr.autofocus]="field.autofocus ? '' : null"
|
||||
[attr.data-autofocus]="field.autofocus ? 'true' : null"
|
||||
[ngModel]="draft[field.key]"
|
||||
(ngModelChange)="setFieldValue(field.key, $event)">
|
||||
</textarea>
|
||||
</mat-form-field>
|
||||
}
|
||||
@case ('select') {
|
||||
<select
|
||||
class="form-select mt-1"
|
||||
[disabled]="field.readonly === true"
|
||||
[attr.autofocus]="field.autofocus ? '' : null"
|
||||
[attr.data-autofocus]="field.autofocus ? 'true' : null"
|
||||
[ngModel]="draft[field.key]"
|
||||
(ngModelChange)="setFieldValue(field.key, $event)">
|
||||
@for (option of field.options ?? []; track option.value) {
|
||||
<option [ngValue]="option.value">{{ option.label }}</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
@case ('checkbox') {
|
||||
<div class="mt-2">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
<mat-form-field appearance="outline" class="mt-1">
|
||||
<mat-label>{{ field.label }}</mat-label>
|
||||
<mat-select
|
||||
[disabled]="field.readonly === true"
|
||||
[attr.autofocus]="field.autofocus ? '' : null"
|
||||
[attr.data-autofocus]="field.autofocus ? 'true' : null"
|
||||
[ngModel]="draft[field.key]"
|
||||
(ngModelChange)="setFieldValue(field.key, $event)" />
|
||||
(ngModelChange)="setFieldValue(field.key, $event)">
|
||||
@for (option of field.options ?? []; track option.value) {
|
||||
<mat-option [value]="option.value">{{ option.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
@case ('checkbox') {
|
||||
<div class="mt-2">
|
||||
<mat-checkbox
|
||||
[disabled]="field.readonly === true"
|
||||
[attr.data-autofocus]="field.autofocus ? 'true' : null"
|
||||
[ngModel]="draft[field.key]"
|
||||
(ngModelChange)="setFieldValue(field.key, $event)">
|
||||
{{ field.label }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
}
|
||||
@default {
|
||||
<input
|
||||
class="form-control mt-1"
|
||||
type="text"
|
||||
[placeholder]="field.placeholder ?? ''"
|
||||
[readonly]="field.readonly === true"
|
||||
[attr.autofocus]="field.autofocus ? '' : null"
|
||||
[attr.data-autofocus]="field.autofocus ? 'true' : null"
|
||||
[ngModel]="draft[field.key]"
|
||||
(ngModelChange)="setFieldValue(field.key, $event)" />
|
||||
<mat-form-field appearance="outline" class="mt-1">
|
||||
<mat-label>{{ field.label }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[placeholder]="field.placeholder ?? ''"
|
||||
[readonly]="field.readonly === true"
|
||||
[attr.autofocus]="field.autofocus ? '' : null"
|
||||
[attr.data-autofocus]="field.autofocus ? 'true' : null"
|
||||
[ngModel]="draft[field.key]"
|
||||
(ngModelChange)="setFieldValue(field.key, $event)" />
|
||||
</mat-form-field>
|
||||
}
|
||||
}
|
||||
|
||||
@if (field.tip) {
|
||||
<small class="text-red-600 mt-1 block">{{ field.tip }}</small>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex justify-end gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel($event)">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="save($event)">Save</button>
|
||||
<button type="button" mat-stroked-button (click)="cancel($event)">Cancel</button>
|
||||
<button type="button" mat-flat-button (click)="save($event)">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { Component, effect, ElementRef, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import {
|
||||
NodeSettingField,
|
||||
NodeSettingsDialogService,
|
||||
|
|
@ -9,7 +16,7 @@ import {
|
|||
@Component({
|
||||
selector: 'app-node-settings-dialog-host',
|
||||
standalone: true,
|
||||
imports: [FormsModule],
|
||||
imports: [FormsModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule, MatTooltipModule],
|
||||
templateUrl: './node-settings-dialog.html'
|
||||
})
|
||||
export class NodeSettingsDialogHostComponent {
|
||||
|
|
|
|||
|
|
@ -2,3 +2,49 @@
|
|||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ordering-root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ordering-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ordering-suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 10px;
|
||||
margin-right: 6px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.ordering-dir-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.ordering-dir-btn:hover {
|
||||
background: #eef2ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.ordering-dir-btn-active {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,34 @@
|
|||
<div class="flex items-center w-full justify-between gap-2">
|
||||
<div class="form-floating flex-1">
|
||||
<select id="orderSelect"
|
||||
class="form-select form-select-sm text-sm w-full"
|
||||
[(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>
|
||||
<div class="ordering-root">
|
||||
<mat-form-field appearance="outline" class="ordering-field">
|
||||
<mat-label>Order by</mat-label>
|
||||
<mat-select [(ngModel)]="orderBy">
|
||||
@for (opt of orderFields(); track opt.field) {
|
||||
<mat-option [value]="opt.field">{{ opt.label || opt.field }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
|
||||
<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 matSuffix class="ordering-suffix">
|
||||
<button
|
||||
type="button"
|
||||
class="ordering-dir-btn"
|
||||
[class.ordering-dir-btn-active]="orderDir() === 'asc'"
|
||||
(mousedown)="setDirection('asc', $event)"
|
||||
(click)="setDirection('asc', $event)"
|
||||
aria-label="Ascending"
|
||||
title="Ascending">
|
||||
<mat-icon fontIcon="north"></mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="ordering-dir-btn"
|
||||
[class.ordering-dir-btn-active]="orderDir() === 'desc'"
|
||||
(mousedown)="setDirection('desc', $event)"
|
||||
(click)="setDirection('desc', $event)"
|
||||
aria-label="Descending"
|
||||
title="Descending">
|
||||
<mat-icon fontIcon="south"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { Component, effect, Input, input, model, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { OrderViewState } from '@utilities/list-state-holder';
|
||||
|
||||
export type OrderEvent = { orderBy: string | null; orderDir: orderDirType };
|
||||
|
|
@ -9,7 +12,7 @@ export type OrderField = { field: string; label?: string };
|
|||
|
||||
@Component({
|
||||
selector: 'app-ordering',
|
||||
imports: [FormsModule],
|
||||
imports: [FormsModule, MatFormFieldModule, MatIconModule, MatSelectModule],
|
||||
templateUrl: './ordering.html',
|
||||
styleUrl: './ordering.css',
|
||||
})
|
||||
|
|
@ -36,6 +39,12 @@ export class Ordering {
|
|||
}
|
||||
}
|
||||
|
||||
setDirection(direction: orderDirType, event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
this.orderDir.set(direction);
|
||||
}
|
||||
|
||||
orderChanged = output<OrderEvent>();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,16 +27,20 @@
|
|||
}
|
||||
|
||||
<div class="mt-3 flex items-start gap-2">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
[type]="isAuthorizationVisible(requirement.key) ? 'text' : 'password'"
|
||||
[readonly]="readOnly()"
|
||||
[ngModel]="authorizationValue(requirement.key)"
|
||||
(ngModelChange)="onAuthorizationValueChange(requirement, $event)" />
|
||||
<mat-form-field appearance="outline" class="flex-1">
|
||||
<mat-label>{{ requirement.fieldName || 'Authorization' }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[type]="isAuthorizationVisible(requirement.key) ? 'text' : 'password'"
|
||||
[readonly]="readOnly()"
|
||||
[ngModel]="authorizationValue(requirement.key)"
|
||||
(ngModelChange)="onAuthorizationValueChange(requirement, $event)" />
|
||||
</mat-form-field>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-sm shrink-0"
|
||||
mat-stroked-button
|
||||
class="shrink-0"
|
||||
[disabled]="readOnly()"
|
||||
(click)="toggleAuthorizationVisibility(requirement.key, $event)">
|
||||
{{ isAuthorizationVisible(requirement.key) ? 'Hide' : 'Show' }}
|
||||
|
|
@ -44,7 +48,8 @@
|
|||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm shrink-0"
|
||||
mat-flat-button
|
||||
class="shrink-0"
|
||||
[disabled]="!canSubmitAuthorization(requirement.key)"
|
||||
(click)="submitAuthorization(requirement, $event)">
|
||||
Save
|
||||
|
|
@ -77,19 +82,18 @@
|
|||
<div class="text-[10px] text-slate-500 mb-2">Type: {{ executionInput.type }}</div>
|
||||
|
||||
@if (isFileInput(executionInput)) {
|
||||
<input
|
||||
type="file"
|
||||
class="form-control form-control-sm"
|
||||
[disabled]="readOnly()"
|
||||
(change)="onFileInputChange(executionInput, $event)" />
|
||||
<input type="file" class="block w-full text-sm text-slate-700" [disabled]="readOnly()" (change)="onFileInputChange(executionInput, $event)" />
|
||||
} @else {
|
||||
<textarea
|
||||
class="form-control form-control-sm"
|
||||
rows="4"
|
||||
[readonly]="readOnly()"
|
||||
[ngModel]="executionInput.value"
|
||||
(ngModelChange)="onTextInputChange(executionInput, $event)">
|
||||
</textarea>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ executionInput.subtitle }}</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
rows="4"
|
||||
[readonly]="readOnly()"
|
||||
[ngModel]="executionInput.value"
|
||||
(ngModelChange)="onTextInputChange(executionInput, $event)">
|
||||
</textarea>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@if (isInputSaving(executionInput.key)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TaskExecutionAuthorizationRequirement } from '@models/task-execution';
|
||||
|
||||
export type EditableExecutionInput = {
|
||||
|
|
@ -15,7 +18,7 @@ export type EditableExecutionInput = {
|
|||
|
||||
@Component({
|
||||
selector: 'app-task-execution-inputs-panel',
|
||||
imports: [CommonModule, FormsModule],
|
||||
imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule],
|
||||
templateUrl: './task-execution-inputs-panel.html'
|
||||
})
|
||||
export class TaskExecutionInputsPanelComponent {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@
|
|||
<button
|
||||
type="button"
|
||||
class="self-stretch h-full w-10 rounded-md border border-emerald-300 bg-emerald-50 text-emerald-600 hover:bg-emerald-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
matTooltip="Start execution"
|
||||
aria-label="Start execution"
|
||||
[disabled]="!canStartExecution() || startInProgress()"
|
||||
(click)="startExecution()">
|
||||
<i class="bi bi-play-fill text-lg leading-none"></i>
|
||||
<mat-icon fontIcon="play_arrow"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -113,7 +114,8 @@
|
|||
@if (output.isLong) {
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 text-xs font-semibold text-blue-600 hover:text-blue-700"
|
||||
mat-stroked-button
|
||||
class="mt-3"
|
||||
(click)="openOutputPreview(output, $event)">
|
||||
View full output
|
||||
</button>
|
||||
|
|
@ -137,7 +139,7 @@
|
|||
<h3 class="text-lg font-semibold text-slate-900">{{ outputModal.title }}</h3>
|
||||
<p class="mt-1 break-all text-xs text-slate-500">{{ outputModal.subtitle }}</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="closeOutputPreview($event)">Close</button>
|
||||
<button type="button" mat-stroked-button (click)="closeOutputPreview($event)">Close</button>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto px-5 py-4">
|
||||
<pre class="execution-output-modal">{{ outputModal.value }}</pre>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, effect, inject, input, OnDestroy, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FlowBlockConnection, FlowData } from '@models/flow';
|
||||
import {
|
||||
getExecutionStatusGroup,
|
||||
|
|
@ -25,7 +28,7 @@ type ExecutionOutputEntry = {
|
|||
|
||||
@Component({
|
||||
selector: 'app-task-execution-viewer',
|
||||
imports: [CommonModule, ReteEditor, TaskExecutionInputsPanelComponent],
|
||||
imports: [CommonModule, ReteEditor, TaskExecutionInputsPanelComponent, MatButtonModule, MatIconModule, MatTooltipModule],
|
||||
templateUrl: './task-execution-viewer.html',
|
||||
styleUrl: './task-execution-viewer.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,3 +2,137 @@
|
|||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tasks-list-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tasks-list-search,
|
||||
.tasks-list-filter,
|
||||
.tasks-list-ordering {
|
||||
padding: 16px 12px 0;
|
||||
}
|
||||
|
||||
.tasks-list-filter {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tasks-list-filter-group {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tasks-list-ordering {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.tasks-list-items {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.tasks-list-list {
|
||||
padding: 0 !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tasks-list-list-item {
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
margin-bottom: 10px;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .tasks-list-list-item .mdc-list-item__content,
|
||||
:host ::ng-deep .tasks-list-list-item .mat-mdc-list-item-unscoped-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.tasks-list-empty {
|
||||
padding: 16px 8px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tasks-list-item {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 14px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(16, 185, 129, 0.1), transparent 32%),
|
||||
linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
box-shadow:
|
||||
0 10px 24px rgba(15, 23, 42, 0.07),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.72);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.tasks-list-item:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: #60a5fa;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(96, 165, 250, 0.16), transparent 34%),
|
||||
linear-gradient(180deg, #f8fbff 0%, #dbeafe 100%);
|
||||
box-shadow: 0 18px 28px rgba(37, 99, 235, 0.14);
|
||||
}
|
||||
|
||||
.tasks-list-item-meta {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tasks-list-item-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.tasks-list-item-subtitle {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.tasks-list-status {
|
||||
padding: 3px 9px;
|
||||
border-radius: 999px;
|
||||
border-width: 1px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.52);
|
||||
}
|
||||
|
||||
.tasks-list-item-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.tasks-list-item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,23 @@
|
|||
<aside class="h-full bg-white border border-slate-200 rounded-md flex flex-col overflow-hidden">
|
||||
<aside class="tasks-list-shell">
|
||||
|
||||
<div class="px-3 pt-4">
|
||||
<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 executions..."
|
||||
[(ngModel)]="searchTerm" />
|
||||
</div>
|
||||
<div class="tasks-list-search">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Search executions</mat-label>
|
||||
<mat-icon matPrefix fontIcon="search"></mat-icon>
|
||||
<input matInput placeholder="Search executions..." [(ngModel)]="searchTerm" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="px-3 pt-4">
|
||||
<div class="btn-group w-full h-8" 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() === 'INIT'"
|
||||
[class.btn-outline-primary]="filter() !== 'INIT'"
|
||||
(click)="filter.set('INIT')">Init</button>
|
||||
<button type="button" class="btn btn-sm w-full transition"
|
||||
[class.btn-primary]="filter() === 'RUNNING'"
|
||||
[class.btn-outline-primary]="filter() !== 'RUNNING'"
|
||||
(click)="filter.set('RUNNING')">Running</button>
|
||||
<button type="button" class="btn btn-sm w-full transition"
|
||||
[class.btn-primary]="filter() === 'FINAL'"
|
||||
[class.btn-outline-primary]="filter() !== 'FINAL'"
|
||||
(click)="filter.set('FINAL')">Final</button>
|
||||
</div>
|
||||
<div class="tasks-list-filter">
|
||||
<mat-button-toggle-group class="tasks-list-filter-group" [value]="filter()" (valueChange)="filter.set($event)">
|
||||
<mat-button-toggle value="all">All</mat-button-toggle>
|
||||
<mat-button-toggle value="INIT">Init</mat-button-toggle>
|
||||
<mat-button-toggle value="RUNNING">Running</mat-button-toggle>
|
||||
<mat-button-toggle value="FINAL">Final</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<div class="px-3 py-4">
|
||||
<div class="tasks-list-ordering">
|
||||
<app-ordering
|
||||
[orderFields]="orderFields"
|
||||
[orderView]="orderView"
|
||||
|
|
@ -44,45 +25,50 @@
|
|||
</app-ordering>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-2 space-y-2">
|
||||
<div class="tasks-list-items">
|
||||
@if (!filteredExecutions().length) {
|
||||
<div class="text-xs text-slate-500 px-2 py-4 text-center">No executions available</div>
|
||||
<div class="tasks-list-empty">No executions available</div>
|
||||
} @else {
|
||||
<mat-list class="tasks-list-list">
|
||||
@for (execution of filteredExecutions(); track execution.id) {
|
||||
<div
|
||||
class="w-full text-left p-3 rounded-md border transition-all duration-150 hover:-translate-y-0.5 hover:border-blue-300 hover:bg-blue-100 hover:shadow-md"
|
||||
<mat-list-item class="tasks-list-list-item">
|
||||
<mat-card
|
||||
class="tasks-list-item"
|
||||
[class.border-blue-300]="execution.id === selectedExecutionId()"
|
||||
[class.bg-blue-50]="execution.id === selectedExecutionId()"
|
||||
[class.border-slate-200]="execution.id !== selectedExecutionId()"
|
||||
[class.bg-white]="execution.id !== selectedExecutionId()"
|
||||
(click)="selectExecution(execution.id)">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium text-slate-800 truncate">{{ execution.title }}</div>
|
||||
<div class="text-xs text-slate-500 truncate">{{ execution.flowName }}</div>
|
||||
<div class="tasks-list-item-meta">
|
||||
<div class="tasks-list-item-title">{{ execution.title }}</div>
|
||||
<div class="tasks-list-item-subtitle">{{ execution.flowName }}</div>
|
||||
</div>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded-full border font-semibold"
|
||||
<span class="tasks-list-status"
|
||||
[ngClass]="statusBadgeClass(execution.status)">
|
||||
{{ execution.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-[11px] text-slate-500 flex justify-between items-center gap-2">
|
||||
<div class="tasks-list-item-footer">
|
||||
<span>{{ execution.startedAt }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="tasks-list-item-actions">
|
||||
<span>{{ execution.duration || '-' }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 rounded enabled:hover:bg-red-100 enabled:text-red-600 disabled:text-red-200"
|
||||
title="Delete"
|
||||
mat-icon-button
|
||||
class="!h-8 !w-8 enabled:!text-red-600 disabled:!text-red-200"
|
||||
matTooltip="Delete execution"
|
||||
aria-label="Delete execution"
|
||||
[disabled]="isDeleteDisabled(execution.status)"
|
||||
(click)="requestDeleteExecution(execution.id, $event)">
|
||||
<i class="bi bi-trash"></i>
|
||||
<mat-icon fontIcon="delete"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
</mat-list-item>
|
||||
}
|
||||
</mat-list>
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, input, model, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { getExecutionStatusGroup, TaskExecutionStatus, TaskExecutionStatusGroup } from '@models/task-execution';
|
||||
import { OrderEvent, OrderField, Ordering, orderDirType } from '@shared/ordering/ordering';
|
||||
import { OrderViewState } from '@utilities/list-state-holder';
|
||||
|
|
@ -18,7 +26,7 @@ export type TaskExecutionListItem = {
|
|||
|
||||
@Component({
|
||||
selector: 'app-tasks-executions-list',
|
||||
imports: [CommonModule, FormsModule, Ordering],
|
||||
imports: [CommonModule, FormsModule, Ordering, MatButtonModule, MatButtonToggleModule, MatCardModule, MatFormFieldModule, MatIconModule, MatInputModule, MatListModule, MatTooltipModule],
|
||||
templateUrl: './tasks-executions-list.html',
|
||||
styleUrl: './tasks-executions-list.css',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,110 @@
|
|||
.title-toolbar-input {
|
||||
width: min(100%, 48vw);
|
||||
.title-toolbar-shell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 8px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.title-toolbar-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title-toolbar-title-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title-toolbar-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
width: fit-content;
|
||||
max-width: min(100%, 32rem);
|
||||
}
|
||||
|
||||
.title-toolbar-field {
|
||||
width: min(100%, 32rem);
|
||||
margin-bottom: -1.25em;
|
||||
}
|
||||
|
||||
.title-toolbar-title-display {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.title-toolbar-edit-button {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
padding: 0 !important;
|
||||
margin-left: 5px;
|
||||
border-radius: 8px !important;
|
||||
color: #1d4ed8 !important;
|
||||
background: rgba(219, 234, 254, 0.95) !important;
|
||||
border: 1px solid rgba(96, 165, 250, 0.55);
|
||||
}
|
||||
|
||||
.title-toolbar-edit-button:hover {
|
||||
background: rgba(191, 219, 254, 0.98) !important;
|
||||
}
|
||||
|
||||
.title-toolbar-edit-button .mat-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.title-toolbar-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.title-toolbar-pill-warning {
|
||||
border-color: #eab308;
|
||||
color: #ca8a04;
|
||||
}
|
||||
|
||||
.title-toolbar-pill-info {
|
||||
border-color: #3b82f6;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.title-toolbar-execute {
|
||||
border-color: #10b981 !important;
|
||||
background: #ecfdf5 !important;
|
||||
color: #047857 !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
|
||||
.title-toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title-toolbar-spinner {
|
||||
|
|
@ -39,3 +144,26 @@
|
|||
.save-snackbar-error {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
.title-toolbar-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px 16px;
|
||||
padding: 8px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.title-toolbar-meta-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.title-toolbar-description {
|
||||
max-width: 420px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,49 @@
|
|||
<div class="flex items-center justify-between gap-4
|
||||
px-4 py-2
|
||||
bg-white border-b border-gray-200
|
||||
shadow-sm">
|
||||
<div class="title-toolbar-shell">
|
||||
|
||||
<!-- Title -->
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<i class="bi bi-pencil-square shrink-0 text-indigo-500"></i>
|
||||
<h2 class="m-0 min-w-0 flex-1 text-sm font-semibold text-gray-800">
|
||||
<input type="text" class="title-toolbar-input border-0 text-sm font-semibold text-gray-800
|
||||
focus:ring-0 p-0 m-0 box-border
|
||||
min-w-0
|
||||
bg-transparent"
|
||||
[value]="title()"
|
||||
(blur)="changeTitle(titleInput.value)"
|
||||
(keydown.enter)="changeTitle(titleInput.value); $event.target.blur()"
|
||||
maxlength="20"
|
||||
size="20"
|
||||
#titleInput
|
||||
/>
|
||||
</h2>
|
||||
<div class="title-toolbar-main">
|
||||
<div class="title-toolbar-title-wrap">
|
||||
@if (editingTitle()) {
|
||||
<mat-form-field appearance="outline" class="title-toolbar-field">
|
||||
<mat-label>Flow title</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[value]="draftTitle()"
|
||||
(input)="draftTitle.set(titleInput.value)"
|
||||
(blur)="changeTitle(titleInput.value)"
|
||||
(keydown.enter)="changeTitle(titleInput.value); $event.preventDefault(); $event.target.blur()"
|
||||
(keydown.escape)="cancelEditingTitle()"
|
||||
maxlength="30"
|
||||
#titleInput />
|
||||
<mat-hint align="end">{{ draftTitle().length }}/30</mat-hint>
|
||||
</mat-form-field>
|
||||
} @else {
|
||||
<div class="title-toolbar-title-row">
|
||||
<div class="title-toolbar-title-display" [title]="title()">{{ title() }}</div>
|
||||
@if (flow()) {
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="title-toolbar-edit-button"
|
||||
matTooltip="Edit flow title"
|
||||
(click)="startEditingTitle()">
|
||||
<mat-icon fontIcon="edit"></mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (notSaved()) {
|
||||
<span class="inline-flex shrink-0 items-center gap-x-1.5 rounded-full border border-yellow-500 px-3 py-1.5 text-xs font-medium text-yellow-500">not saved</span>
|
||||
<span class="title-toolbar-pill title-toolbar-pill-warning">Not saved</span>
|
||||
} @else if (blockSyncInProgress()) {
|
||||
<span class="inline-flex shrink-0 items-center gap-2 rounded-full border border-blue-500 px-3 py-1.5 text-xs font-medium text-blue-600">
|
||||
<span class="title-toolbar-pill title-toolbar-pill-info">
|
||||
<span class="title-toolbar-spinner title-toolbar-spinner--blue" aria-hidden="true"></span>
|
||||
<span>Updating blocks...</span>
|
||||
</span>
|
||||
} @else if (canExecute()) {
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex shrink-0 items-center gap-2 rounded-full border border-emerald-500 bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 transition enabled:hover:bg-emerald-100 disabled:cursor-default disabled:opacity-60"
|
||||
mat-stroked-button
|
||||
class="title-toolbar-execute"
|
||||
[disabled]="executeLoading()"
|
||||
(click)="execute()">
|
||||
@if (executeLoading()) {
|
||||
|
|
@ -40,29 +54,26 @@
|
|||
}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex shrink-0 items-center gap-2" >
|
||||
<button tabindex="-1"
|
||||
class="border-0
|
||||
flex items-center gap-1
|
||||
enabled:hover:bg-green-200 enabled:text-green-600 disabled:text-green-300
|
||||
p-1"
|
||||
title="Save" [disabled]="!canSave()" (click)="save()">
|
||||
<i class="bi bi-save"></i>
|
||||
<div class="title-toolbar-actions" >
|
||||
<button
|
||||
type="button"
|
||||
mat-flat-button
|
||||
tabindex="-1"
|
||||
[disabled]="!canSave()"
|
||||
(click)="save()">
|
||||
Save
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (flow()) {
|
||||
<div class="px-4 py-2 bg-white border-b border-gray-200 text-xs text-slate-600 flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
<span><span class="text-slate-400">Author:</span> {{ flow()!.author }}</span>
|
||||
<span><span class="text-slate-400">Visibility:</span> {{ flow()!.visibility }}</span>
|
||||
<span><span class="text-slate-400">Created:</span> {{ flow()!.createdAt | date:'dd/MM/yyyy HH:mm' }}</span>
|
||||
<span><span class="text-slate-400">Updated:</span> {{ flow()!.updatedAt | date:'dd/MM/yyyy HH:mm' }}</span>
|
||||
<div class="title-toolbar-meta">
|
||||
<span><span class="title-toolbar-meta-label">Author:</span> {{ flow()!.author }}</span>
|
||||
<span><span class="title-toolbar-meta-label">Visibility:</span> {{ flow()!.visibility }}</span>
|
||||
<span><span class="title-toolbar-meta-label">Created:</span> {{ flow()!.createdAt | date:'dd/MM/yyyy HH:mm' }}</span>
|
||||
<span><span class="title-toolbar-meta-label">Updated:</span> {{ flow()!.updatedAt | date:'dd/MM/yyyy HH:mm' }}</span>
|
||||
@if (flow()!.description) {
|
||||
<span class="truncate max-w-[420px]"><span class="text-slate-400">Description:</span> {{ flow()!.description }}</span>
|
||||
<span class="title-toolbar-description"><span class="title-toolbar-meta-label">Description:</span> {{ flow()!.description }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, ElementRef, inject, signal, ViewChild } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { Router } from '@angular/router';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { TaskExecutionsService } from '@services/task-executions/task-executions';
|
||||
|
|
@ -8,7 +14,7 @@ import { EditorStateHolder } from '@stores/flow-editor';
|
|||
|
||||
@Component({
|
||||
selector: 'app-title-toolbar',
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatIconModule, MatInputModule, MatTooltipModule],
|
||||
templateUrl: './title-toolbar.html',
|
||||
styleUrl: './title-toolbar.css',
|
||||
})
|
||||
|
|
@ -37,17 +43,41 @@ export class TitleToolbar {
|
|||
executeLoading = signal(false);
|
||||
snackbarMessage = signal<string | null>(null);
|
||||
snackbarType = signal<'success' | 'error'>('success');
|
||||
editingTitle = signal(false);
|
||||
draftTitle = signal('');
|
||||
|
||||
startEditingTitle() {
|
||||
const flow = this.flow();
|
||||
if (!flow) return;
|
||||
this.draftTitle.set(flow.name);
|
||||
this.editingTitle.set(true);
|
||||
queueMicrotask(() => {
|
||||
this.myInputRef?.nativeElement?.focus();
|
||||
this.myInputRef?.nativeElement?.select();
|
||||
});
|
||||
}
|
||||
|
||||
cancelEditingTitle() {
|
||||
this.editingTitle.set(false);
|
||||
this.draftTitle.set(this.title());
|
||||
}
|
||||
|
||||
changeTitle(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (value === this.title()) return;
|
||||
if (trimmed === this.title()) {
|
||||
this.editingTitle.set(false);
|
||||
return;
|
||||
}
|
||||
if (trimmed.length < 4) {
|
||||
this.draftTitle.set(this.title());
|
||||
this.myInputRef.nativeElement.value = this.title();
|
||||
this.editingTitle.set(false);
|
||||
return;
|
||||
}
|
||||
console.log('Updating flow title to:', trimmed);
|
||||
this.editorState.updateFlowTitle(trimmed );
|
||||
this.draftTitle.set(trimmed);
|
||||
this.editingTitle.set(false);
|
||||
}
|
||||
|
||||
save() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
@import '@fontsource/material-icons-outlined';
|
||||
@import '@angular/material/prebuilt-themes/azure-blue.css';
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
|
|
@ -31,6 +33,35 @@
|
|||
|
||||
body {
|
||||
@apply bg-gray-50 dark:bg-gray-900;
|
||||
color: #0f172a;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.mat-mdc-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-surface,
|
||||
.mat-mdc-card {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.material-icons,
|
||||
.material-icons-outlined {
|
||||
font-family: 'Material Icons Outlined' !important;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-feature-settings: 'liga';
|
||||
}
|
||||
|
||||
.container-centered {
|
||||
|
|
@ -86,4 +117,4 @@ body {
|
|||
}
|
||||
.inner-nav {
|
||||
@apply flex flex-wrap justify-center gap-4 items-center max-w-7xl;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue