changes related to session expire warning, session time out issue, exchange rate search label and modal resize-able
This commit is contained in:
@@ -4,7 +4,6 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Router } from '@angular/router';
|
||||
import { merge, fromEvent, Subscription } from 'rxjs';
|
||||
import { throttleTime } from 'rxjs/operators';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { AuthService } from '../../../features/authentication/data-access/auth.service';
|
||||
import { TokenStorageService } from './token-storage.service';
|
||||
|
||||
@@ -19,9 +18,6 @@ export class SessionTimeoutService implements OnDestroy {
|
||||
readonly showWarning = signal(false);
|
||||
readonly remainingSeconds = signal(0);
|
||||
|
||||
private readonly configWarningAfterMs = environment.sessionTimeout?.warningAfterMs ?? 25 * 60 * 1000;
|
||||
private readonly configLogoutAfterMs = environment.sessionTimeout?.logoutAfterMs ?? 30 * 60 * 1000;
|
||||
|
||||
private activitySubscription: Subscription | null = null;
|
||||
private warningTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private logoutTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -109,8 +105,15 @@ export class SessionTimeoutService implements OnDestroy {
|
||||
this.showWarning.set(false);
|
||||
this.remainingSeconds.set(0);
|
||||
|
||||
// Calculate effective delays based on token expiry if available
|
||||
const { warningDelay, logoutDelay } = this.calculateEffectiveDelays();
|
||||
// Delays are derived solely from the access token expiry issued by the backend.
|
||||
const delays = this.calculateEffectiveDelays();
|
||||
|
||||
if (!delays) {
|
||||
// No token expiry info available - nothing to schedule client-side.
|
||||
return;
|
||||
}
|
||||
|
||||
const { warningDelay, logoutDelay } = delays;
|
||||
|
||||
const safeWarningDelay = Math.max(Math.min(warningDelay, logoutDelay), 0);
|
||||
const safeLogoutDelay = Math.max(logoutDelay, 0);
|
||||
@@ -130,19 +133,14 @@ export class SessionTimeoutService implements OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate effective warning/logout delays based on the sooner of:
|
||||
* - Configured timeout values
|
||||
* - Actual token expiry from backend
|
||||
* Derive warning/logout delays purely from the access token's expiry
|
||||
* timestamp issued by the backend - no client-side hard-coded durations.
|
||||
*/
|
||||
private calculateEffectiveDelays(): { warningDelay: number; logoutDelay: number } {
|
||||
private calculateEffectiveDelays(): { warningDelay: number; logoutDelay: number } | null {
|
||||
const accessTokenExpiresOn = this.tokenStorage.getAccessTokenExpiresOn();
|
||||
|
||||
if (!accessTokenExpiresOn) {
|
||||
// No token expiry info - use configured values
|
||||
return {
|
||||
warningDelay: this.configWarningAfterMs,
|
||||
logoutDelay: this.configLogoutAfterMs
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenExpiryMs = Date.parse(accessTokenExpiresOn);
|
||||
@@ -155,14 +153,10 @@ export class SessionTimeoutService implements OnDestroy {
|
||||
|
||||
const tokenTimeRemaining = tokenExpiryMs - now;
|
||||
|
||||
// Use the sooner of configured timeout or token expiry
|
||||
// Warning at 80% of token lifetime or configured warning, whichever is sooner
|
||||
const tokenBasedWarning = Math.floor(tokenTimeRemaining * 0.8);
|
||||
const tokenBasedLogout = tokenTimeRemaining;
|
||||
|
||||
// Warn at 80% of the remaining token lifetime.
|
||||
return {
|
||||
warningDelay: Math.min(this.configWarningAfterMs, tokenBasedWarning),
|
||||
logoutDelay: Math.min(this.configLogoutAfterMs, tokenBasedLogout)
|
||||
warningDelay: Math.floor(tokenTimeRemaining * 0.8),
|
||||
logoutDelay: tokenTimeRemaining
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
[totalRecords]="tableStore.filteredRecords()" [pageIndex]="tableStore.queryState.pageIndex()"
|
||||
[pageSize]="tableStore.queryState.pageSize()"
|
||||
[initialSortColumn]="tableStore.queryState.sortColumn()" [initialSortDirection]="tableStore.queryState.sortDirection()"
|
||||
tableTitle="Exchange Rates (ROE)" buttonTitle="Add"
|
||||
tableTitle="Exchange Rates" buttonTitle="Add"
|
||||
[showSearch]="true" [showAddButton]="true" [showFilterButton]="true" [filterActive]="showFilters()"
|
||||
searchPlaceholder="Search pair..." [searchDebounceTime]="300" toolTip="Add New Rate" (addClicked)="onAddRate()"
|
||||
searchPlaceholder="Search rates..." [searchDebounceTime]="300" toolTip="Add New Rate" (addClicked)="onAddRate()"
|
||||
(searchChanged)="tableStore.onSearch($event)" (pageChanged)="tableStore.onPageChange($event)"
|
||||
(sortChanged)="tableStore.onSortChange($event)" (actionClicked)="onActionClick($event)"
|
||||
(filterClicked)="onToggleFilters()">
|
||||
|
||||
@@ -1,62 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
<app-data-table
|
||||
[columns]="columns()"
|
||||
[rows]="tableStore.rows()"
|
||||
[actions]="actions()"
|
||||
[totalRecords]="tableStore.totalRecords()"
|
||||
[pageIndex]="tableStore.queryState.pageIndex()"
|
||||
[pageSize]="tableStore.queryState.pageSize()"
|
||||
tableTitle="States"
|
||||
buttonTitle="Add"
|
||||
[showSearch]="true"
|
||||
[showAddButton]="true"
|
||||
[showFilterButton]="true"
|
||||
[filterActive]="showFilters()"
|
||||
searchPlaceholder="Search states..."
|
||||
[searchDebounceTime]="300"
|
||||
toolTip="Add State"
|
||||
(addClicked)="onAddState()"
|
||||
(filterClicked)="onToggleFilters()"
|
||||
(searchChanged)="tableStore.onSearch($event)"
|
||||
(pageChanged)="tableStore.onPageChange($event)"
|
||||
(sortChanged)="tableStore.onSortChange($event)"
|
||||
(actionClicked)="onActionClick($event)"
|
||||
>
|
||||
<ng-template appDataTableToolbar>
|
||||
<form [formGroup]="countryFilterForm" (ngSubmit)="onApplyFilter($event)" autocomplete="off" class="flex flex-nowrap items-end gap-3">
|
||||
<div class="w-[220px] shrink-0">
|
||||
<app-autocomplete
|
||||
formControlName="countryId"
|
||||
inputId="state-country-filter"
|
||||
variant="floating"
|
||||
size="sm"
|
||||
label="Country"
|
||||
placeholder="Search"
|
||||
[searchFn]="searchCountries"
|
||||
[displayWith]="displayCountry"
|
||||
[valueWith]="countryValue"
|
||||
[selectedItem]="selectedCountryLookup()"
|
||||
[minSearchLength]="1"
|
||||
[debounceTime]="300"
|
||||
[limit]="50"
|
||||
[clearable]="true"
|
||||
[hideValidation]="true"
|
||||
wrapperClass="!mb-0 w-full"
|
||||
(itemSelected)="onFilterCountrySelected($event)"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
<ng-template appDataTableFilterActions>
|
||||
<button type="button" class="ti-btn ti-btn-primary-full !rounded-full !m-0 flex min-h-8 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap !px-3 !py-1.5 !text-[0.75rem]" (click)="onApplyFilter($event)">
|
||||
<span>Apply</span>
|
||||
</button>
|
||||
<button type="button" class="ti-btn ti-btn-light !rounded-full !m-0 flex min-h-8 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap !px-3 !py-1.5 !text-[0.75rem]" (click)="onResetFilter()">
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
=======
|
||||
<app-data-table [columns]="columns()" [rows]="tableStore.rows()" [actions]="actions()"
|
||||
[totalRecords]="tableStore.totalRecords()" [pageIndex]="tableStore.queryState.pageIndex()"
|
||||
[pageSize]="tableStore.queryState.pageSize()"
|
||||
@@ -88,7 +29,6 @@
|
||||
|
||||
<app-confirm-dialog title="Delete State" text="Do you really want to delete this state?" confirmButtonText="Delete"
|
||||
cancelButtonText="Cancel" (confirmed)="onDeleteConfirmed()" (cancelled)="onDeleteCancelled()" />
|
||||
>>>>>>> dev
|
||||
|
||||
<app-state-form-modal [open]="tableStore.showModal()" [mode]="tableStore.modalMode()"
|
||||
[stateId]="tableStore.selectedItem()?.id ?? null" (saved)="tableStore.refresh(); tableStore.closeModal()"
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Subject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, finalize, switchMap } from 'rxjs/operators';
|
||||
import { NotificationService } from '../../../../../core/services/common/notification.service';
|
||||
|
||||
import { CountryLookupDto, CountryService } from '../../../countries/public-api';
|
||||
import { StateDto, UpdateStateRequest } from '../../models/state.model';
|
||||
import { CountryService } from '../../../countries/public-api';
|
||||
import { StateDto } from '../../models/state.model';
|
||||
import { StateService } from '../../data-access/state.service';
|
||||
import { DataTable, DataTableToolbarDirective } from '../../../../../shared/components/data-table/data-table';
|
||||
import { DataTableStore } from '../../../../../shared/components/data-table/data-table.store';
|
||||
@@ -133,9 +133,6 @@ export class StateList implements OnInit {
|
||||
this.showFilters.update(value => !value);
|
||||
}
|
||||
|
||||
onFilterCountrySelected(country: CountryLookupDto | null): void {
|
||||
this.selectedCountryLookup.set(country);
|
||||
this.countryFilterForm.controls.countryId.setValue(country ? country.id : '');
|
||||
onCountrySearchChanged(term: string): void {
|
||||
this.countrySearch$.next(term);
|
||||
}
|
||||
|
||||
@@ -434,7 +434,7 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:host-context(.dark) .data-table-modern .ti-pagination li .page-link {
|
||||
:host-context(.dark) .data-table-modern .ti-pagination li .page-link,
|
||||
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-page,
|
||||
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-prev,
|
||||
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-next {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
::ng-deep .form-control:focus {
|
||||
border-color: var(--color-primary);
|
||||
border-color: var(--color-primary) !important;
|
||||
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--color-primary) 20%, transparent);
|
||||
}
|
||||
|
||||
|
||||
@@ -308,8 +308,9 @@
|
||||
.shared-form-input::placeholder { color: transparent; }
|
||||
|
||||
.shared-form-control.is-focused .shared-form-input,
|
||||
.shared-form-control.has-value .shared-form-input {
|
||||
border-color: var(--color-primary);
|
||||
.shared-form-control.has-value .shared-form-input,
|
||||
.shared-form-input:focus {
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.shared-floating-label {
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
tabindex="-1"
|
||||
(mousedown)="$event.stopPropagation()">
|
||||
|
||||
<div class="modern-modal">
|
||||
<div class="modern-modal" [style.width]="modalInitialWidth()">
|
||||
|
||||
@if (showHeader()) {
|
||||
<div class="modern-modal-header">
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -113,10 +113,18 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
min-width: 320px;
|
||||
min-height: 220px;
|
||||
max-width: 96vw;
|
||||
max-height: 90vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
overflow: hidden;
|
||||
resize: both;
|
||||
|
||||
&::-webkit-resizer {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
border-radius: 20px;
|
||||
|
||||
@@ -920,6 +928,8 @@
|
||||
border-radius: 18px;
|
||||
|
||||
max-height: 95vh;
|
||||
width: 100% !important;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.modal-header-content {
|
||||
|
||||
@@ -40,16 +40,16 @@ export class Modal implements OnDestroy {
|
||||
readonly closed = output<void>();
|
||||
readonly submitted = output<void>();
|
||||
|
||||
readonly modalSizeClass = computed(() => {
|
||||
const sizes: Record<ModalSize, string> = {
|
||||
sm: 'max-w-md',
|
||||
md: 'max-w-2xl',
|
||||
lg: 'max-w-4xl',
|
||||
xl: 'max-w-6xl',
|
||||
full: 'max-w-[96vw]'
|
||||
readonly modalInitialWidth = computed(() => {
|
||||
const widths: Record<ModalSize, string> = {
|
||||
sm: '28rem',
|
||||
md: '42rem',
|
||||
lg: '56rem',
|
||||
xl: '72rem',
|
||||
full: '96vw'
|
||||
};
|
||||
|
||||
return sizes[this.size()];
|
||||
return widths[this.size()];
|
||||
});
|
||||
|
||||
readonly modalBoxClass = computed(() => {
|
||||
@@ -60,8 +60,7 @@ export class Modal implements OnDestroy {
|
||||
'ease-out',
|
||||
'relative',
|
||||
'z-[1]',
|
||||
'pointer-events-auto',
|
||||
this.modalSizeClass()
|
||||
'pointer-events-auto'
|
||||
].join(' ');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
export interface AppSessionTimeoutConfig {
|
||||
warningAfterMs: number;
|
||||
logoutAfterMs: number;
|
||||
}
|
||||
|
||||
export interface AppEnvironment {
|
||||
production: boolean;
|
||||
api: {
|
||||
identity: string;
|
||||
masterAdmin: string;
|
||||
};
|
||||
sessionTimeout: AppSessionTimeoutConfig;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,4 @@ export const environment: AppEnvironment = {
|
||||
identity: '/api',
|
||||
masterAdmin: '/api',
|
||||
},
|
||||
sessionTimeout: {
|
||||
warningAfterMs: 25 * 60 * 1000,
|
||||
logoutAfterMs: 30 * 60 * 1000,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,10 +12,6 @@ export const environment: AppEnvironment = {
|
||||
// identity: 'https://identity.yourdomain.com/api',
|
||||
// masterAdmin: 'https://master-admin.yourdomain.com/api',
|
||||
},
|
||||
sessionTimeout: {
|
||||
warningAfterMs: 25 * 60 * 1000,
|
||||
logoutAfterMs: 30 * 60 * 1000,
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
|
||||
</head>
|
||||
|
||||
|
||||
+1
-3
@@ -2,9 +2,7 @@
|
||||
@forward "../public/assets/css/style.css";
|
||||
@forward "../node_modules/ngx-toastr/toastr.css";
|
||||
|
||||
@import "flatpickr/dist/flatpickr.css";
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600&display=swap');
|
||||
@use "flatpickr/dist/flatpickr.css";
|
||||
|
||||
/* Etihad Altis Text (local font) */
|
||||
@font-face {
|
||||
|
||||
Reference in New Issue
Block a user