Compare commits

...

14 Commits

Author SHA1 Message Date
Valentin Mihai
7612d524da RED-8711 - fixed toaster message 2024-04-15 18:57:24 +03:00
Valentin Mihai
f3697a27ef lint 2024-03-25 15:38:01 +02:00
Valentin Mihai
8b304a7279 add a devInfo method to toaster service 2024-03-25 15:36:46 +02:00
Nicoleta Panaghiu
d6f7122329 RED-7558: backport generic errors. 2023-11-02 16:36:25 +02:00
Adina Țeudan
7312caa8d0 Added some updates from master branch 2023-10-02 11:38:49 +03:00
Nicoleta Panaghiu
ac3ccb1d7d RED-7605: Fixed page header overlap. 2023-09-19 12:10:21 +03:00
Dan Percic
57375e2489 Merge branch 'RED-7528' into 'release-3.988.0'
RED-7528: Fixed documents jumping around on drag&drop.

See merge request fforesight/shared-ui-libraries/common-ui!6
2023-09-05 14:09:01 +02:00
Nicoleta Panaghiu
85c4ac984d RED-7528: Fixed documents jumping around on drag&drop. 2023-09-05 14:59:42 +03:00
Adina Țeudan
2a9c43fc6b RED-6343: Shift+click selection in table (anywhere on list item) 2023-06-22 13:01:41 +03:00
Valentin Mihai
406f7b1fdd RED-6412 - INC15516657: Performance issues 2023-05-18 13:42:15 +03:00
Nicoleta Panaghiu
cb5647bc8d RED-6428: Created help mode link for Edit file attribute. 2023-04-10 12:38:39 +03:00
Valentin Mihai
9819ed892b lint 2023-04-06 13:44:54 +03:00
Valentin Mihai
7ba2c8f3a3 RED-6453 - Value for attribute in file list cannot be set 2023-04-06 13:42:48 +03:00
Valentin Mihai
d1e095ce6a RED-6420 - No refreshes anymore for new user notifications 2023-03-28 16:28:57 +03:00
20 changed files with 162 additions and 32 deletions

View File

@ -86,17 +86,13 @@ section.settings {
}
.fullscreen {
.page-header {
position: absolute;
top: 0;
}
.content-inner {
height: calc(100% - 50px);
}
.overlay-shadow {
top: 50px;
.right-container {
transform: translateY(61px);
height: calc(100% - 61px);
}
}

View File

@ -36,4 +36,8 @@
&.mdc-list-item--disabled {
color: rgba(var(--iqser-text-rgb), 0.7);
}
> span {
width: 100%;
}
}

View File

@ -1,9 +1,10 @@
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpStatusCode } from '@angular/common/http';
import { inject, Inject, Injectable, Optional } from '@angular/core';
import { MonoTypeOperatorFunction, Observable, retry, throwError, timer } from 'rxjs';
import { Inject, Injectable, Optional } from '@angular/core';
import { finalize, MonoTypeOperatorFunction, Observable, retry, throwError, timer } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { MAX_RETRIES_ON_SERVER_ERROR, SERVER_ERROR_SKIP_PATHS } from './tokens';
import { ErrorService } from './error.service';
import { LoadingService } from '../loading';
import { KeycloakService } from 'keycloak-angular';
import { IqserConfigService } from '../services';
@ -49,6 +50,7 @@ export class ServerErrorInterceptor implements HttpInterceptor {
private readonly _errorService: ErrorService,
private readonly _keycloakService: KeycloakService,
private readonly _configService: IqserConfigService,
private readonly _loadingService: LoadingService,
@Optional() @Inject(MAX_RETRIES_ON_SERVER_ERROR) private readonly _maxRetries: number,
@Optional() @Inject(SERVER_ERROR_SKIP_PATHS) private readonly _skippedPaths: string[],
) {}
@ -70,7 +72,11 @@ export class ServerErrorInterceptor implements HttpInterceptor {
this._urlsWithError.add(req.url);
}
return throwError(() => error);
return throwError(() => error).pipe(
finalize(() => {
this._loadingService.stop();
}),
);
}),
backoffOnServerError(this._maxRetries, this._skippedPaths),
tap(() => {

View File

@ -3,7 +3,7 @@ import { combineLatest, Observable, pipe } from 'rxjs';
import { IFilter } from '../models/filter.model';
import { INestedFilter } from '../models/nested-filter.model';
import { IFilterGroup } from '../models/filter-group.model';
import { handleCheckedValue } from '../filter-utils';
import { extractFilterValues, handleCheckedValue } from '../filter-utils';
import { FilterService } from '../filter.service';
import { SearchService } from '../../search';
import { Filter } from '../models/filter';
@ -18,6 +18,17 @@ const atLeastOneIsExpandable = pipe(
shareDistinctLast(),
);
export interface LocalStorageFilter {
id: string;
checked: boolean;
children?: LocalStorageFilter[] | null;
}
export interface LocalStorageFilters {
primaryFilters: LocalStorageFilter[] | null;
secondaryFilters: LocalStorageFilter[] | null;
}
@Component({
selector: 'iqser-filter-card [primaryFiltersSlug]',
templateUrl: './filter-card.component.html',
@ -36,6 +47,7 @@ const atLeastOneIsExpandable = pipe(
})
export class FilterCardComponent implements OnInit {
@Input() primaryFiltersSlug!: string;
@Input() fileId?: string;
@Input() actionsTemplate?: TemplateRef<unknown>;
@Input() secondaryFiltersSlug = '';
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
@ -54,7 +66,7 @@ export class FilterCardComponent implements OnInit {
private readonly _elementRef: ElementRef,
) {}
private get _primaryFilters$(): Observable<IFilter[]> {
get #primaryFilters$(): Observable<IFilter[]> {
return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe(
map(([group]) => this.searchService.searchIn(group?.filters ?? [])),
shareLast(),
@ -64,7 +76,7 @@ export class FilterCardComponent implements OnInit {
ngOnInit() {
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareLast());
this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug).pipe(shareLast());
this.primaryFilters$ = this._primaryFilters$;
this.primaryFilters$ = this.#primaryFilters$;
this.atLeastOneFilterIsExpandable$ = atLeastOneIsExpandable(this.primaryFilterGroup$);
this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$);
@ -96,16 +108,17 @@ export class FilterCardComponent implements OnInit {
}
this.filterService.refresh();
this.#updateFiltersInLocalStorage();
}
activatePrimaryFilters(): void {
this._setFilters(this.primaryFiltersSlug, true);
this.#setFilters(this.primaryFiltersSlug, true);
}
deactivateFilters(exceptedFilterId?: string): void {
this._setFilters(this.primaryFiltersSlug, false, exceptedFilterId);
this.#setFilters(this.primaryFiltersSlug, false, exceptedFilterId);
if (this.secondaryFiltersSlug) {
this._setFilters(this.secondaryFiltersSlug, false, exceptedFilterId);
this.#setFilters(this.secondaryFiltersSlug, false, exceptedFilterId);
}
}
@ -116,7 +129,7 @@ export class FilterCardComponent implements OnInit {
this.filterService.refresh();
}
private _setFilters(filterGroup: string, checked = false, exceptedFilterId?: string) {
#setFilters(filterGroup: string, checked = false, exceptedFilterId?: string) {
const filters = this.filterService.getGroup(filterGroup)?.filters;
filters?.forEach(f => {
if (f.id !== exceptedFilterId) {
@ -130,4 +143,21 @@ export class FilterCardComponent implements OnInit {
});
this.filterService.refresh();
}
#updateFiltersInLocalStorage(): void {
if (this.fileId) {
const primaryFilters = this.filterService.getGroup('primaryFilters');
const secondaryFilters = this.filterService.getGroup('secondaryFilters');
const filters: LocalStorageFilters = {
primaryFilters: extractFilterValues(primaryFilters?.filters),
secondaryFilters: extractFilterValues(secondaryFilters?.filters),
};
const workloadFiltersString = localStorage.getItem('workload-filters') ?? '{}';
const workloadFilters = JSON.parse(workloadFiltersString);
workloadFilters[this.fileId] = filters;
localStorage.setItem('workload-filters', JSON.stringify(workloadFilters));
}
}
}

View File

@ -4,6 +4,7 @@ import { IFilterGroup } from './models/filter-group.model';
import { IFilter } from './models/filter.model';
import { NestedFilter } from './models/nested-filter';
import { Id, IListable } from '../listing';
import { LocalStorageFilter } from './filter-card/filter-card.component';
function copySettings(oldFilters: INestedFilter[], newFilters: INestedFilter[]) {
if (!oldFilters || !newFilters) {
@ -102,3 +103,25 @@ export function flatChildren(filters: INestedFilter[]): IFilter[] {
export function toFlatFilters(groups: IFilterGroup[], condition = (filters: IFilter[]) => filters): IFilter[] {
return groups.reduce((acc: IFilter[], f) => [...acc, ...condition(f.filters), ...condition(flatChildren(f.filters))], []);
}
export function extractFilterValues(filters: INestedFilter[] | undefined): LocalStorageFilter[] | null {
const extractedValues: LocalStorageFilter[] = [];
filters?.forEach(filter => {
extractedValues.push({
id: filter.id,
checked: filter.checked ?? false,
children: extractFilterValues(filter.children),
});
});
return extractedValues.length ? extractedValues : null;
}
export function copyLocalStorageFiltersValues(primaryFilters: INestedFilter[], localStorageFilters: LocalStorageFilter[]) {
primaryFilters?.forEach(filter => {
const localStorageFilter = localStorageFilters.find(f => f.id === filter.id);
filter.checked = localStorageFilter?.checked;
if (filter.children && localStorageFilter?.children) {
copyLocalStorageFiltersValues(filter.children, localStorageFilter.children);
}
});
}

View File

@ -36,6 +36,7 @@
[primaryFiltersLabel]="primaryFiltersLabel"
[primaryFiltersSlug]="primaryFiltersSlug"
[secondaryFiltersSlug]="secondaryFiltersSlug"
[fileId]="fileId"
></iqser-filter-card>
</ng-template>
</div>

View File

@ -14,6 +14,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
})
export class PopupFilterComponent implements OnInit {
@Input() primaryFiltersSlug!: string;
@Input() fileId?: string;
@Input() actionsTemplate?: TemplateRef<unknown>;
@Input() secondaryFiltersSlug = '';
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');

View File

@ -3,3 +3,7 @@
width: 100%;
}
}
::selection {
background: #accef7;
}

View File

@ -60,7 +60,7 @@
overflow-y: auto !important;
&.has-scrollbar iqser-table-item::ng-deep {
.action-buttons {
.action-buttons:not(.edit-button) {
right: 0;
padding-right: 10px;
}

View File

@ -47,7 +47,7 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
multiSelect(entity: Class, $event: MouseEvent): void {
if (this.selectionEnabled && this._multiSelectActive$.value) {
$event.stopPropagation();
this.listingService.select(entity);
this.listingService.select(entity, $event.shiftKey);
}
}
@ -80,6 +80,7 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
@HostListener('window:keydown.control')
@HostListener('window:keydown.meta')
@HostListener('window:keydown.shift')
private _enableMultiSelect() {
this._multiSelectActive$.next(true);
}
@ -88,6 +89,7 @@ export class TableContentComponent<Class extends IListable<PrimaryKey>, PrimaryK
@HostListener('window:blur')
@HostListener('window:keyup.control')
@HostListener('window:keyup.meta')
@HostListener('window:keyup.shift')
private _disableMultiSelect() {
this._multiSelectActive$.next(false);
}

View File

@ -63,8 +63,11 @@
}
.action-buttons {
position: absolute;
display: none !important;
}
.action-buttons:not(.edit-button) {
position: absolute;
right: -11px;
top: 0;
height: 100%;

View File

@ -51,13 +51,9 @@
<div *ngFor="let e of ctx.draggingEntities" [style.min-height]="itemHeight + 'px'" class="placeholder"></div>
</ng-template>
<ng-template cdkDragPreview>
<ng-template cdkDragPreview [matchSize]="true">
<ng-container *ngFor="let e of ctx.draggingEntities">
<div
[class.selected]="all[e.id].isSelected$ | async"
[ngClass]="all[e.id].classes$ | async"
[style.width]="itemWidth + 'px'"
>
<div [class.selected]="all[e.id].isSelected$ | async" [ngClass]="all[e.id].classes$ | async">
<ng-container *ngTemplateOutlet="itemTemplate; context: { entity: e }"></ng-container>
</div>
</ng-container>

View File

@ -4,7 +4,8 @@ import { Observable } from 'rxjs';
import { HeadersConfiguration, List, RequiredParam, Validate } from '../utils';
import { map } from 'rxjs/operators';
const ROOT_CHANGES_KEY = 'root';
export const ROOT_CHANGES_KEY = 'root';
export const LAST_CHECKED_OFFSET = 15000;
export interface HeaderOptions {
readonly authorization?: boolean;
@ -23,7 +24,9 @@ export interface QueryParam {
*/
export abstract class GenericService<I> {
protected readonly _http = inject(HttpClient);
protected readonly _lastCheckedForChanges = new Map<string, string>([[ROOT_CHANGES_KEY, new Date().toISOString()]]);
protected readonly _lastCheckedForChanges = new Map<string, string>([
[ROOT_CHANGES_KEY, new Date(Date.now() - LAST_CHECKED_OFFSET).toISOString()],
]);
protected abstract readonly _defaultModelPath: string;
get<T = I[]>(): Observable<T>;
@ -144,6 +147,6 @@ export abstract class GenericService<I> {
}
protected _updateLastChanged(key = ROOT_CHANGES_KEY): void {
this._lastCheckedForChanges.set(key, new Date().toISOString());
this._lastCheckedForChanges.set(key, new Date(Date.now() - LAST_CHECKED_OFFSET).toISOString());
}
}

View File

@ -15,6 +15,11 @@ export abstract class IqserUserPreferenceService extends GenericService<UserAttr
protected abstract readonly _devFeaturesEnabledKey: string;
#userAttributes: UserAttributes = {};
get isIqserDevMode(): boolean {
const value = sessionStorage.getItem(this._devFeaturesEnabledKey);
return value === 'true';
}
get userAttributes(): UserAttributes {
return this.#userAttributes;
}
@ -74,3 +79,7 @@ export class DefaultUserPreferenceService extends IqserUserPreferenceService {
protected readonly _defaultModelPath = 'attributes';
protected readonly _devFeaturesEnabledKey = inject(BASE_HREF) + '.enable-dev-features';
}
export function isIqserDevMode() {
return inject(IqserUserPreferenceService).isIqserDevMode;
}

View File

@ -6,6 +6,7 @@ import { TranslateService } from '@ngx-translate/core';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { filter } from 'rxjs/operators';
import { ErrorMessageService } from './error-message.service';
import { isIqserDevMode } from './iqser-user-preference.service';
import { DomSanitizer } from '@angular/platform-browser';
const enum NotificationType {
@ -26,6 +27,7 @@ export interface ToasterOptions extends IndividualConfig {
*/
readonly params?: Record<string, string | number>;
readonly actions?: ToasterActions[];
readonly useRaw?: boolean;
}
export interface ErrorToasterOptions extends ToasterOptions {
@ -35,10 +37,19 @@ export interface ErrorToasterOptions extends ToasterOptions {
readonly error?: HttpErrorResponse;
}
const defaultDevToastOptions: Partial<ToasterOptions> = {
timeOut: 10000,
easing: 'ease-in-out',
easeTime: 500,
useRaw: true,
};
@Injectable({
providedIn: 'root',
})
export class Toaster {
readonly #isIqserDevMode = isIqserDevMode();
constructor(
private readonly _toastr: ToastrService,
private readonly _router: Router,
@ -62,6 +73,18 @@ export class Toaster {
return this._toastr.error(resultedMsg, options?.title, options);
}
rawError(message: string, config?: Partial<IndividualConfig<unknown>>) {
return this._toastr.error(message, undefined, config);
}
devInfo(message: string): ActiveToast<unknown> | undefined {
if (!this.#isIqserDevMode) {
return;
}
return this.info(message, defaultDevToastOptions);
}
info(message: string, options?: Partial<ToasterOptions>): ActiveToast<unknown> {
return this._showToastNotification(message, NotificationType.INFO, options);
}
@ -79,7 +102,7 @@ export class Toaster {
notificationType = NotificationType.INFO,
options?: Partial<ToasterOptions>,
): ActiveToast<unknown> {
const translatedMsg = this._translateService.instant(message, options?.params) as string;
const translatedMsg = options?.useRaw ? message : (this._translateService.instant(message, options?.params) as string);
switch (notificationType) {
case NotificationType.SUCCESS:

View File

@ -1,10 +1,10 @@
import { NgModule } from '@angular/core';
import { LogoComponent, SideNavComponent, SmallChipComponent, StatusBarComponent } from './index';
import { LogoComponent, SideNavComponent, SmallChipComponent } from './index';
import { CommonModule } from '@angular/common';
import { MatTooltipModule } from '@angular/material/tooltip';
import { IqserIconsModule } from '../icons';
const components = [SmallChipComponent, StatusBarComponent, SideNavComponent, LogoComponent];
const components = [SmallChipComponent, SideNavComponent, LogoComponent];
@NgModule({
declarations: [...components],

View File

@ -1,5 +1,7 @@
import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
import { StatusBarConfig } from './status-bar-config.model';
import { NgClass, NgForOf, NgIf, NgStyle } from '@angular/common';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'iqser-status-bar',
@ -7,6 +9,8 @@ import { StatusBarConfig } from './status-bar-config.model';
styleUrls: ['./status-bar.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgClass, NgStyle, NgForOf, MatTooltipModule, NgIf],
})
export class StatusBarComponent<T extends string> {
@Input() configs: readonly StatusBarConfig<T>[] = [];

View File

@ -2,3 +2,4 @@ export * from './pipes/log.pipe';
export * from './pipes/humanize-camel-case.pipe';
export * from './pipes/capitalize.pipe';
export * from './pipes/humanize.pipe';
export * from './pipes/size.pipe';

View File

@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
import { size } from '../../utils';
@Pipe({
name: 'size',
standalone: true,
})
export class SizePipe implements PipeTransform {
transform(value: number): string {
return size(value);
}
}

View File

@ -25,6 +25,18 @@ export function humanizeCamelCase(value: string): string {
return value.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
}
export function size(value: number): string {
if (value >= 1000 ** 3) {
return `${(value / 1000 ** 3).toFixed(2)} GB`;
}
if (value >= 1000 ** 2) {
return `${(value / 1000 ** 2).toFixed(2)} MB`;
}
return `${(value / 1000).toFixed(2)} KB`;
}
export function escapeHtml<T extends unknown | string>(unsafe: T, options?: { ignoreTags: string[] }) {
if (typeof unsafe !== 'string') {
return unsafe;