UNPKG

@controladad/ng-base

Version:
1,220 lines (1,198 loc) 548 kB
import * as i0 from '@angular/core'; import { InjectionToken, inject, Injectable, signal, computed, EventEmitter, Component, ViewChild, Input, Output, HostBinding, ElementRef, ChangeDetectionStrategy, HostListener, effect, DestroyRef, input, ContentChildren, Directive, PLATFORM_ID, Inject, Optional, Self, Host, Injector, runInInjectionContext, ViewChildren, Pipe, ChangeDetectorRef, provideAppInitializer, importProvidersFrom } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import * as i2 from '@angular/material/icon'; import { MatIconRegistry, MatIconModule, MatIcon } from '@angular/material/icon'; import { createStore, withProps, select, getStore, enableElfProdMode } from '@ngneat/elf'; import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { persistState, localStorageStrategy, sessionStorageStrategy } from '@ngneat/elf-persist-state'; import { Observable, of, tap, filter, pipe, map, distinctUntilChanged, Subject, debounceTime, startWith, BehaviorSubject, Subscription, merge, debounce, timer, fromEvent, take, combineLatest, switchMap as switchMap$1, catchError, NEVER, retry, throwError, interval } from 'rxjs'; import { RouterLink, Router, ResolveStart, NavigationEnd } from '@angular/router'; import * as jalali from 'date-fns-jalali'; import { addDays, addMonths, addYears, toDate, getMonth, parseISO, format, getDate, getDay, setDay, setMonth, getDaysInMonth, getYear, parse, set } from 'date-fns-jalali'; import * as dateFns from 'date-fns'; import { switchMap } from 'rxjs/operators'; import * as i1$c from '@angular/material/dialog'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialog, MatDialogModule } from '@angular/material/dialog'; import * as i1$2 from '@angular/forms'; import { ReactiveFormsModule, Validators, FormGroup, FormControl, UntypedFormControl, FormsModule } from '@angular/forms'; import * as i1 from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button'; import * as i2$1 from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule, MatProgressSpinner } from '@angular/material/progress-spinner'; import * as i1$6 from '@angular/common'; import { NgStyle, NgTemplateOutlet, AsyncPipe, isPlatformServer, DatePipe, NgClass, DecimalPipe, formatNumber, Location, registerLocaleData } from '@angular/common'; import * as i3 from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip'; import * as i1$1 from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { formControl, isFormControlExtended, formGroup, Validators as Validators$1, provideFormErrors } from '@al00x/forms'; import { trigger, transition, style, animate } from '@angular/animations'; import * as i1$4 from '@angular/material/menu'; import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; import * as i6 from '@angular/material/datepicker'; import { MatDatepickerModule, DateRange, MAT_DATE_RANGE_SELECTION_STRATEGY, DefaultMatCalendarRangeStrategy } from '@angular/material/datepicker'; import * as i2$2 from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field'; import * as i1$5 from '@angular/material/input'; import { MatInputModule } from '@angular/material/input'; import { MatChipsModule } from '@angular/material/chips'; import * as i8 from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import * as i1$3 from '@angular/material/radio'; import { MatRadioButton, MatRadioModule } from '@angular/material/radio'; import tippy from 'tippy.js'; import Inputmask from 'inputmask'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import * as i9 from 'ngx-quill'; import { QuillModule } from 'ngx-quill'; import * as i3$1 from '@angular/cdk/bidi'; import * as i1$7 from '@al00x/screen-detector'; import { ScreenDetectorService } from '@al00x/screen-detector'; import * as i1$8 from '@angular/material/slider'; import { MatSliderModule } from '@angular/material/slider'; import * as i1$9 from '@angular/material/slide-toggle'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import * as i1$a from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs'; import * as i1$b from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import * as i1$d from '@al00x/printer'; import { AlxPrintModule } from '@al00x/printer'; import * as i1$e from '@angular/material/snack-bar'; import { MAT_SNACK_BAR_DATA, MatSnackBarModule } from '@angular/material/snack-bar'; import * as i1$f from '@angular/material/table'; import { MatColumnDef, MatCellDef, MatHeaderCellDef, MatFooterCellDef, MatCell, MatHeaderCell, MatTableModule } from '@angular/material/table'; import { MatBadge, MatBadgeModule } from '@angular/material/badge'; import * as i2$3 from '@angular/cdk/a11y'; import { A11yModule } from '@angular/cdk/a11y'; import { HttpClient, HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import * as i1$g from 'ngx-file-drop'; import { NgxFileDropModule } from 'ngx-file-drop'; import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core'; import { faIR } from 'date-fns-jalali/locale'; import { provideAnimations } from '@angular/platform-browser/animations'; import { DateFnsAdapter } from '@angular/material-date-fns-adapter'; import { loadTranslations } from '@angular/localize'; import { enUS } from 'date-fns/locale'; import localeEn from '@angular/common/locales/en'; const API_BASEURL = new InjectionToken('API_BASEURL'); const ENVIRONMENT = new InjectionToken('ENVIRONMENT'); // prettier-ignore const baseIcons = [ 'add', 'arrow-down', 'arrow-up', 'arrow-left', 'arrow-right', 'calendar', 'camera', 'check', 'check-double', 'chevron-down', 'chevron-left', 'chevron-right', 'chevron-up', 'clock', 'close', 'delete', 'dropdown', 'edit', 'edit-box', 'error', 'excel-file', 'eye', 'eye-slash', 'filter', 'filter-filled', 'hashtag', 'info', 'info-circle', 'list', 'location', 'location-check', 'login', 'logout', 'menu', 'nav', 'numeric-down', 'numeric-up', 'paper', 'paper-details', 'password', 'phone', 'plate', 'play', 'plus', 'power', 'print', 'question-circle', 'refresh', 'reports', 'save', 'search', 'settings', 'sort', 'sort-down', 'sort-up', 'time', 'trash', 'trash-alt', 'user', 'user-circle', 'users', 'wrench' ]; let isBaseRegistered = false; function registerIcons(icons) { const sanitizer = inject(DomSanitizer); const iconRegistry = inject(MatIconRegistry); if (!isBaseRegistered) { for (const icon of baseIcons) { iconRegistry.addSvgIcon(icon, sanitizer.bypassSecurityTrustResourceUrl(`./assets/base/icons/${icon}.svg`)); } isBaseRegistered = true; } for (const icon of icons ?? []) { iconRegistry.addSvgIcon(icon, sanitizer.bypassSecurityTrustResourceUrl(`./assets/icons/${icon}.svg`)); } } class CacGlobalConfig { // Do not change this variable manually, it's automatically updated via the provider. static { this.defaultLang = 'en'; } static { this.applicationName = ''; } static { this.localization = { langs: ['en'], }; } // applies application name to store keys, for example if your store key is `auth`, it will become `app_name_auth` static { this.applyPrefixToStorageKeys = true; } static generateStoreKey(key) { return `${this.applyPrefixToStorageKeys && this.applicationName.length ? `${this.applicationName}_` : ''}${key}`; } } class ApiInterceptor { constructor() { this.apiBaseUrl = inject(API_BASEURL); } // using inject() causes circular error, idk why... // readonly apiBaseUrl = inject(API_BASEURL); intercept(request, next) { request = request.clone({ url: request.url.startsWith('/') ? this.apiBaseUrl + request.url : request.url, }); return next.handle(request); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: ApiInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: ApiInterceptor }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: ApiInterceptor, decorators: [{ type: Injectable }] }); class BaseStore { constructor(storeOpts) { this.storeOpts = storeOpts; const key = storeOpts.exactKey ? storeOpts.key : CacGlobalConfig.generateStoreKey(storeOpts.key); const store = createStore({ name: key }, withProps(storeOpts.default ?? {})); persistState(store, { key: key, storage: storeOpts.storageStrategy ?? localStorageStrategy, }); this.store = store; this.state$ = this.store.pipe(); try { this.signal = toSignal(this.state$); } catch { /* empty */ } } get state() { return this.get(); } get() { return this.store.getValue(); } patch(value) { return this.store.update((s) => ({ ...s, ...value, })); } } class _AppBaseStore extends BaseStore { constructor(defaults) { super({ key: 'app', default: defaults, }); this.rememberMe$ = this.store.pipe(select(this.rememberMe)); this.lang$ = this.store.pipe(select(this.lang)); } rememberMe() { return this.get().rememberMe ?? false; } setRememberMe(value) { // @ts-ignore this.patch({ rememberMe: value, }); } lang() { return this.get().lang ?? CacGlobalConfig.defaultLang; } setLang(value) { // @ts-ignore this.patch({ lang: value, }); } } // This is dummy, used to make service out of the `_AppBaseStore`. For extension, `_AppBaseStore` should be used class AppBaseStore extends _AppBaseStore { constructor() { super(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AppBaseStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AppBaseStore, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AppBaseStore, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [] }); const SERVER_ERROR = { 0: $localize `:@@base.errors.server.0:Error connecting, Check your network connection.`, 401: $localize `:@@base.errors.server.401:Your session has expired, please login again.`, 403: $localize `:@@base.errors.server.403:You do not have permission to continue.`, 404: $localize `:@@base.errors.server.404:The requested destination was not found.`, 500: $localize `:@@base.errors.server.500:An error occurred on the server.`, 503: $localize `:@@base.errors.server.503:The server is unavailable.`, default: $localize `:@@base.errors.server.default:Error.`, }; const API_ERROR = { 1: $localize `:@@base.errors.api.default:Username or Password is incorrect`, }; class ErrorHelper { static getResponseErrorMessage(serverCode, apiError) { return serverCode === 400 && apiError ? this.getApiErrorMessage(apiError) : this.getServerErrorMessage(serverCode); } static getServerErrorMessage(status) { return SERVER_ERROR[status] ?? SERVER_ERROR.default; } static getApiErrorMessage(error) { return API_ERROR[error.code] ?? error.message; } // we pass the error object, returned by the api to this function static parseApiErrorObject(error) { if (!error) return undefined; if (typeof error === 'object') { if ('error' in error && typeof error.error === 'string') { return error.error; } if ('errors' in error && typeof error.errors === 'object') { const message = this.extractMessageFromObjectRecord(error.errors); return message ? { code: -1, message, } : undefined; } if ('error' in error && typeof error === 'object' && 'code' in error.error && 'message' in error.error) { return this.parseApiErrorObject(error.error); } return error; } else if (typeof error === 'string') { try { return JSON.parse(error); } catch { return { code: -1, message: error, }; } } return undefined; } static extractMessageFromObjectRecord(object) { const values = Object.values(object); for (const value of values) { if (value instanceof Array && value.length > 0) { return value[0]; } else if (typeof value === 'string') { return value; } } return undefined; } } let _cachedDateFns; // Get DateFns library based on current language function DateFns() { if (_cachedDateFns) return _cachedDateFns; if (CacGlobalConfig.localization.forceDateFnsLib === 'jalali') _cachedDateFns = jalali; else if (CacGlobalConfig.localization.forceDateFnsLib === 'georgian') _cachedDateFns = dateFns; else { const store = getStore(CacGlobalConfig.generateStoreKey('app')); _cachedDateFns = (store?.getValue().lang === 'fa' ? jalali : dateFns); } return _cachedDateFns; } function getDatesIntervalInHHMMSS(start, end, showSymbol) { const hours = Math.abs(DateFns().differenceInHours(start, end)); const duration = DateFns().intervalToDuration({ start, end }); const result = { hours, minutes: duration.minutes, seconds: duration.seconds, negative: start > end, }; return { formatted: `${result.hours?.toFixed().padStart(2, '0')}:${result.minutes ?.toFixed() .padStart(2, '0')}:${result.seconds?.toFixed().padStart(2, '0')}${showSymbol ? ` (${result.negative ? '-' : '+'})` : ''}`, ...result, }; } function getFormattedDate(date, format = 'yyyy/MM/dd') { return !date ? '' : DateFns().format(typeof date === 'string' ? new Date(date) : date, format); } function parseDate(date, format) { return DateFns().parse(date, format, new Date()); } function getDurationInHHMM(minutes) { const h = `${Math.floor(minutes / 60)}`.padStart(2, '0'); const m = `${minutes % 60}`.padStart(2, '0'); return `${h}:${m}`; } function getHHMMInDuration(text) { const split = text.split(':'); const h = split.at(0); const m = split.at(1); if (!h || !m) return 0; const parsedH = parseInt(h); const parsedM = parseInt(m); if (isNaN(parsedH) || isNaN(parsedM)) return 0; return parseInt(h) * 60 + parseInt(m); } function getFullName(obj, defaultValue = '') { const firstNameKeys = ['firstName', 'first_name', 'firstname', 'FirstName']; const lastNameKeys = ['lastName', 'last_name', 'lastname', 'LastName']; if (!obj) return defaultValue; const firstNameKey = firstNameKeys.find((key) => key in obj); const lastNameKey = lastNameKeys.find((key) => key in obj); const name = `${firstNameKey ? (obj[firstNameKey] ?? '') : ''} ${lastNameKey ? (obj[lastNameKey] ?? '') : ''}`.trim(); return name.length ? name : defaultValue; } function toPascalCase(text) { let result = text ?? ''; if (result.length) { result = result.charAt(0).toUpperCase() + result.slice(1); } return result; } async function lazyLoad(component, selector) { const entry = await component; if (selector) return selector(entry); const props = Object.values(entry); if (props.length) return props[0]; console.error('LAZY LOAD ERROR', entry); throw new Error('Entry has no exported components!!'); } function resolveRouteChildren(route) { if (route.children) return route.children; // _loadedRoutes is a private property of Route which holds the list of lazy loaded routes config. we need to use it! // @ts-ignore if (route._loadedRoutes) return route._loadedRoutes; } function isRouteExtended(r) { return 'layout' in r; } function effectDep(dep, fn, destroyRef) { return toObservable(dep).pipe(takeUntilDestroyed(destroyRef)).subscribe(fn); } function clone(o) { return structuredClone(o); } const objectToId = (t) => ('id' in t ? t.id : typeof t === 'object' ? Object.values(t).at(0) : t); function omit(obj, ...keys) { keys.forEach((key) => delete obj[key]); return obj; } function isObject(item) { return item && typeof item === 'object' && !Array.isArray(item); } function deepMerge(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); if (isObject(target) && isObject(source)) { for (const key in source) { if (isObject(source[key])) { if (!target[key]) Object.assign(target, { [key]: {} }); deepMerge(target[key], source[key]); } else { Object.assign(target, { [key]: source[key] }); } } } return deepMerge(target, ...sources); } function getObservable(value) { return value instanceof Observable ? value : of(value); } function startWithTap(callback) { return (source) => of({}).pipe(tap(callback), switchMap(() => source)); } function filterEmpty() { return (source) => source.pipe(filter((x) => x instanceof Array ? x.filter((t) => t !== undefined && t !== null).length === x.length : x !== undefined && x !== null)); } function distinctUntilChangedWithTimeout(timeout, compare) { return pipe(map((t) => ({ value: t, date: new Date() })), distinctUntilChanged((pre, cur) => (compare ? compare(pre?.value, cur?.value) : pre?.value === cur?.value) && pre.date.getTime() > cur.date.getTime() - timeout), map((t) => t.value)); } function destroyRefObservable(ref) { return new Observable((observer) => { ref.onDestroy(observer.next.bind(observer)); }); } function getFromItemRecord(items, value) { return items.find((t) => t.value === value || (t.alt !== undefined && t.alt === value)); } function flatten(array) { return [].concat(...array); } function includes(array, terms) { if (!(terms instanceof Array)) return array.includes(terms); for (const term of terms) { if (array.includes(term)) return true; } return false; } function arraySafeAt(array, index) { if (index === null || index === undefined) return null; return array.at(index) ?? null; } function dedupe(array) { return [...new Set(array)]; } function dedupeObj(array, key) { return array.filter((value, index) => index === array.findIndex((t) => t[key] === value[key])); } function subset(array, sub) { return sub.every((val) => array.includes(val)); } function arraysEqual(a, b) { if (a === b) return true; if (a == null || b == null) return false; if (a.length !== b.length) return false; for (let i = 0; i < a.length; ++i) { if (a[i] !== b[i]) return false; } return true; } const ACTION_TYPES = ['read', 'create', 'update', 'delete', 'print', 'export', 'output', 'other']; function getAllActions() { return ACTION_TYPES; } function permissionNameToKey(name, action) { switch (action) { case 'create': return `Add${name}`; case 'update': return `Update${name}`; case 'read': return `Get${name}`; case 'delete': return ''; case 'print': return `PrintListOf${name}`; case 'export': return `ExportAll${name}`; case 'output': return `${name}Output`; case `other`: return `${name}`; } } function permissionActionToLabel(action) { switch (action) { case 'create': return $localize `:@@base.permissions.crudActions.create:Add`; case 'update': return $localize `:@@base.permissions.crudActions.update:Edit`; case 'read': return $localize `:@@base.permissions.crudActions.read:View Only`; case 'delete': return $localize `:@@base.permissions.crudActions.delete:Delete`; case 'print': return $localize `:@@base.permissions.crudActions.print:Print`; case 'export': return $localize `:@@base.permissions.crudActions.export:Export excel`; case 'output': return $localize `:@@base.permissions.crudActions.output:Export`; case `other`: return ''; } } function isClass(obj) { const isCtorClass = obj.constructor && obj.constructor.toString().substring(0, 5) === 'class'; if (obj.prototype === undefined) { return isCtorClass; } const isPrototypeCtorClass = obj.prototype.constructor && obj.prototype.constructor.toString && obj.prototype.constructor.toString().substring(0, 5) === 'class'; return isCtorClass || isPrototypeCtorClass; } function provide(token, value, multi = false) { // @ts-ignore if (typeof value === 'function') { return { provide: token, useFactory: value, multi }; } else if (isClass(value)) { return { provide: token, useClass: value, multi }; } else { return { provide: token, useValue: value, multi }; } } function componentWithDefaultConfig(component, token, defaultValues = {}) { const valuesInjected = injectOptional(token); if (!valuesInjected) return; const values = valuesInjected instanceof Array ? valuesInjected : [valuesInjected]; let defaults = { ...defaultValues, }; for (const val of values) { defaults = { ...defaults, ...val, }; } for (const key in defaults) { // @ts-ignore component[key] = defaults[key]; } } function injectOptional(token) { try { return inject(token); } catch { return undefined; } } class SortModel { constructor(key, direction) { this._key = signal(undefined); this._direction = signal('desc'); this._changes$ = new Subject(); this.key = this._key.asReadonly(); this.direction = this._direction.asReadonly(); this.changes$ = this._changes$.pipe(debounceTime(50)); this._key.set(key); this._direction.set(direction ?? 'desc'); } setKey(sort) { this._key.set(sort); this.emit(); } setDirection(direction) { this._direction.set(direction); this.emit(); } create() { const key = this.key(); return key ? { key: key, direction: this.direction(), } : undefined; } emit() { this._changes$.next([this._key(), this._direction()]); } } // TODO: Update constructor initial parameters (turn into object, it's messy currently) // TODO: add formControl binding // TODO: turn off multiple mode by default class SelectionModel { constructor(itemsCount, multiple, initial, itemToId) { this._totalCount = 0; this._selectedCount = 0; this._currentViewItems = []; this._blockBoundUpdate = false; this.indeterminate = signal(false); this.allSelected = signal(false); this.selected = signal([]); this.selectedIds = signal({}); this.selectedCount = computed(() => this.selected()?.length ?? 0); this.hasSelection = computed(() => this.selectedCount() !== 0); this.id = crypto.randomUUID(); this._totalCount = itemsCount ?? 0; this._multiple = multiple ?? true; this._itemToId = itemToId ?? ((t) => (t && typeof t === 'object' && 'id' in t ? t.id : t)); initial ? this.select(...initial) : null; } get isMultiple() { return this._multiple; } select(...items) { if (this._multiple) { this.set(...[...new Set([...this.selected(), ...items])]); } else if (items.length) { // this.clear(); this.set(items[0]); } } deselect(...items) { const itemsId = items.map(this._itemToId); this.set(...this.selected().filter((t) => !itemsId.includes(this._itemToId(t)))); } toggle(...items) { const selected = this.selected(); if (this._multiple) { let newItems = [...selected]; if (newItems.length > 0) { for (const item of items) { const index = selected.findIndex((t) => this._itemToId(t) === this._itemToId(item)); if (index !== -1) { newItems.splice(index, 1); } else { newItems.push(item); } } } else { newItems = [...items]; } this.set(...newItems); } else { if (selected.some(t => t === items[0])) { this.clear(); } else { this.set(items[0]); } } } selectAll() { this.select(...this._currentViewItems); } deselectAll() { this.deselect(...this._currentViewItems); } toggleAll() { if (this._selectedCount === this._totalCount) { this.deselectAll(); } else { this.selectAll(); } } clear() { if (this._selectedCount === 0) return; this.set(); } set(...items) { this.selected.set(items); this.selectedIds.set(items .map((t) => this._itemToId(t)) .reduce((pre, cur) => ({ ...pre, [cur]: true, }), {})); this._selectedCount = items.length; this.calculateSelectionState(); this.updateBoundedFormControl(); } isSelected(item) { return !!this.selectedIds()[this._itemToId(item)]; } setTotalCount(count) { this._totalCount = count; this.calculateSelectionState(); } setItems(items, setTotalCount = true) { this._currentViewItems = items; if (setTotalCount) { this._totalCount = items.length; } this.calculateSelectionState(); } setItemToIdFn(fn) { this._itemToId = fn; } setMultiple(value) { this._multiple = value === undefined ? true : value; } bindFormControl(control, destroyRef) { this._boundFormControl = control; if (!control) return; control.valueChanges.pipe(startWith(control.value), takeUntilDestroyed(destroyRef)).subscribe((v) => { if (this._blockBoundUpdate || v === this.selectedIds()) { this._blockBoundUpdate = false; return; } if (v === null || v === undefined) { this.clear(); } else { this.set(...(v instanceof Array ? v : [v])); } }); } updateBoundedFormControl() { if (!this._boundFormControl) return; this._blockBoundUpdate = true; this._boundFormControl.setValue(this.selectedIds()); this._boundFormControl.setSelectedItems(this.selected()); } calculateSelectionState() { const selectedViewItemsCount = this._currentViewItems .map(this._itemToId) .filter((t) => this.selectedIds()[t]).length; if (this._selectedCount === 0 || selectedViewItemsCount === 0) { this.indeterminate.set(false); this.allSelected.set(false); } else if (this._selectedCount === this._totalCount) { this.indeterminate.set(false); this.allSelected.set(true); } else { this.indeterminate.set(selectedViewItemsCount !== this._currentViewItems.length); this.allSelected.set(!this.indeterminate()); } } } const ActiveValues = [ { value: true, label: 'Active' }, { value: false, label: 'Inactive' }, ]; const SuspendedValues = [ { value: false, label: 'Active' }, { value: true, label: 'Inactive' }, ]; const BooleanValues = [ { value: true, label: 'Yes' }, { value: false, label: 'No' }, ]; class FilterModel { constructor(init, overrideEmpty) { this.overrideEmpty = overrideEmpty; this._filters = signal({}); this._changes$ = new Subject(); this.filters = computed(() => { const obj = this._filters(); if (!obj) return this.overrideEmpty; Object.keys(obj).forEach((key) => obj[key] === undefined && delete obj[key]); if (Object.keys(obj).length === 0) return this.overrideEmpty; return obj; }); this.filtersArray = computed(() => { const filters = this.filters(); if (!filters) return []; return Object.values(filters); }); this.hasFilter = computed(() => { return this.filters() !== undefined; }); this.changes$ = this._changes$.pipe(debounceTime(50)); this._filters.set(init ?? overrideEmpty ?? {}); } set(data, values, emit = true) { if (values === undefined || values.every((t) => t.value === undefined || t.value === null)) { this.remove(data.prop); return; } this._filters.set({ ...this._filters(), [data.prop]: this.createFilterItem(data, values), }); if (emit) this.emitChanges(); } remove(prop) { this._filters.set({ ...this._filters(), [prop]: undefined, }); this.emitChanges(); } clear() { this._filters.set({}); this.emitChanges(); } create() { const filters = this.filtersArray(); return filters .filter((t) => !!t.values && t.values.length > 0) .map((item) => item.values.map((v) => ({ key: v.key ?? item.key, strictKey: !!item.strictKey || !!v.key, value: this.mapValueToString(v.value), type: v.type, }))) .reduce((pre, cur) => pre.concat(cur), []); } // Emit will be called automatically, use this is you disabled emit on any functions emitChanges() { this._changes$.next(this._filters()); } createFilterItem(opts, value) { const item = { ...this.getKey(opts), values: undefined, prop: opts.prop, label: opts.label, icon: opts.icon, }; return this.setValueForFilterItem(item, value, opts.items); } setValueForFilterItem(item, values, records) { const newValues = values ? values.every((t) => t.value === null || t.value === undefined) ? undefined : values.map((t) => { const value = t.value; if (value instanceof Date && t.controlType === 'date') { if (t.type === 'lower') { value.setHours(23, 59, 59, 999); } else { value.setHours(0, 0, 0, 0); } } return { ...t, value, }; }) : undefined; item.values = newValues; item.formatted = newValues ? this.formatFilterItem(item, records) : undefined; return item; } mapValueToString(value) { if (value instanceof Array) { return value.map((t) => this.mapValueToString(t)); } if (value instanceof Date) { return value.toISOString(); } if (typeof value === 'boolean') { return value ? 'true' : 'false'; } if (typeof value === 'number') { return value.toString(); } return value ?? ''; } mapValueToReadable(filterValue, records) { const value = filterValue.value; if (value instanceof Date) { if (filterValue.controlType === 'datetime') { return getFormattedDate(value, 'HH:mm ,yyyy/MM/dd'); } return getFormattedDate(value); } if (filterValue.displayText && filterValue.displayText.length) { return filterValue.displayText instanceof Array ? filterValue.displayText.join(', ') : filterValue.displayText; } if (records && records instanceof Array) { return getFromItemRecord(records, value)?.label ?? ''; } if (typeof value === 'boolean') { return value ? $localize `:@@base.values.trueText:Yes` : $localize `:@@base.values.falseText:No`; } if (typeof value === 'number') { return value.toString(); } return value instanceof Array ? (value.length ? value.join(', ') : '') : value ?? ''; } getKey(data) { if (data.filterable === true) { return { key: data.prop }; } else if (typeof data.filterable === 'string') { return data.filterable !== '' ? { key: data.filterable, strictKey: true } : { key: data.prop }; } return { key: data.prop }; } formatFilterItem(item, records) { let prefix = ''; let suffix = ''; const equal = item.values?.find((t) => t.type === 'equal' || t.type === 'contains'); const greater = item.values?.find((t) => t.type === 'greater'); const lower = item.values?.find((t) => t.type === 'lower'); const equalValue = equal?.value !== undefined && equal?.value !== null ? this.mapValueToReadable(equal, records) : undefined; const greaterValue = greater?.value !== undefined && greater?.value !== null ? this.mapValueToReadable(greater, records) : undefined; const lowerValue = lower?.value !== undefined && lower?.value !== null ? this.mapValueToReadable(lower, records) : undefined; if (equalValue !== undefined && equalValue !== null) { suffix += ` : ${equalValue}`; } else if (greaterValue !== undefined && greaterValue !== null && lowerValue !== undefined && lowerValue !== null) { prefix += `${greaterValue} < `; suffix += ` < ${lowerValue}`; } else if (greaterValue !== undefined && greaterValue !== null) { suffix += ` > ${greaterValue}`; } else if (lowerValue !== undefined && lowerValue !== null) { suffix += ` < ${lowerValue}`; } if (prefix === '' && suffix === '') return undefined; return { full: `${prefix}${item.label}${suffix}`, text: item.label, prefix: prefix, suffix: suffix, }; } } class TableFilterModel extends FilterModel { constructor(initValue, overrideEmptyValue) { super(initValue); this.initValue = initValue; this.overrideEmptyValue = overrideEmptyValue; this.columnFilters = {}; this.columnLabels = {}; this.columnsChanged$ = new BehaviorSubject(undefined); } set(columnProp, values, emit = true) { if (typeof columnProp === 'object') { super.set(columnProp, values, emit); return; } if (!this._columns) return; const col = this._columns.find((t) => t.prop === columnProp); if (!col) { console.error(`Table filter failed with the given prop: '${columnProp}'`); return; } super.set(col, values, emit); } setColumns(columns) { this._columns = columns; this.updateFilters(); this.setOverrideEmpty(); this.columnsChanged$.next(); } getColumnFilters(columnProp) { return this.columnFilters[columnProp]; } setOverrideEmpty() { if (!this.overrideEmptyValue || !this._columns) return; this.overrideEmpty = {}; for (const prop in this.overrideEmptyValue) { const column = this._columns.find((t) => t.prop === prop); if (!column) continue; const value = this.overrideEmptyValue[prop]; const item = this.createFilterItem(column, value); this.overrideEmpty[item.prop] = item; } if (!this.initValue) { this._filters.set(this.overrideEmpty); } } updateFilters() { if (!this._columns) return; this.columnFilters = {}; this.columnLabels = {}; for (const column of this._columns) { let filters; if (column.filterable instanceof Array) { filters = column.filterable; } else if (column.filterable) { switch (column.type ?? 'text') { case 'number': { filters = [ { inputType: 'number', type: 'equal' }, { inputType: 'number', type: 'greater' }, { inputType: 'number', type: 'lower' }, ]; break; } case 'boolean': { filters = [{ type: 'equal', controlType: 'select', items: column.items ?? BooleanValues }]; break; } case 'plate': filters = [{ type: 'equal', controlType: 'plate' }]; break; default: { filters = [ { type: 'contains', controlType: column.items ? 'select' : 'input', items: column.items ?? undefined, }, ]; } } } if (filters) { this.columnLabels[column.prop] = column.label; this.columnFilters[column.prop] = filters; } } } } const ICON_COMPONENT_CONFIG = new InjectionToken('IconComponent'); class CacIconComponent { constructor() { this.disabled = false; this.size = '1.5rem'; this.strokeWidth = 1.9; this.onClick = new EventEmitter(); this.pointerEventsNone = false; this.thisWidth = ''; this.thisHeight = ''; this.isClickable = signal(false); componentWithDefaultConfig(this, ICON_COMPONENT_CONFIG); this.thisWidth = this.size; this.thisHeight = this.size; } ngOnInit() { this.isClickable.set(this.onClick.observed); } ngAfterViewInit() { // @ts-ignore const fetchSub = this.matIcon._currentIconFetch; fetchSub.add(() => { const svgElement = this.matIcon._elementRef.nativeElement.children.item(0); if (!svgElement) return; svgElement.style.strokeWidth = `${this.strokeWidth}`; }); } ngOnChanges(changes) { if (changes['disabled']) { this.pointerEventsNone = this.disabled; } if (changes['size']) { this.thisWidth = this.size; this.thisHeight = this.size; } } onClickEvent(e) { if (this.disabled) return; this.onClick.emit(e); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: CacIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.7", type: CacIconComponent, isStandalone: true, selector: "cac-icon", inputs: { icon: "icon", disabled: "disabled", size: "size", strokeWidth: "strokeWidth", iconClass: "iconClass", wrapperClass: "wrapperClass" }, outputs: { onClick: "onClick" }, host: { properties: { "class.pointer-events-none": "this.pointerEventsNone", "style.width": "this.thisWidth", "style.height": "this.thisHeight" } }, viewQueries: [{ propertyName: "matIcon", first: true, predicate: ["MatIcon"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n role=\"presentation\"\n class=\"ui-icon relative {{ disabled ? 'disabled' : '' }} w-full h-full {{ wrapperClass }}\"\n [class.is-clickable]=\"isClickable()\"\n (click)=\"onClickEvent($event)\"\n >\n @if (!!icon) {\n <mat-icon\n #MatIcon\n class=\"{{ iconClass ?? '' }}\"\n [ngStyle]=\"{ width: size, height: size }\"\n [svgIcon]=\"icon\"\n ></mat-icon>\n }\n <ng-content></ng-content>\n</div>\n", styles: [":host{display:inline-block;color:inherit;position:relative;border-radius:.25rem}:host ::ng-deep .ui-icon{height:inherit;width:inherit;color:inherit;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;display:inline-flex;align-items:center;justify-content:center;border-radius:inherit}:host ::ng-deep .ui-icon.is-clickable{cursor:pointer}:host ::ng-deep .ui-icon.is-clickable:hover{background-color:var(--surface-bright)!important;--tw-brightness: brightness(.9) !important;--tw-drop-shadow: drop-shadow(0 1px 1px rgb(0 0 0 / .05)) !important;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}:host ::ng-deep .ui-icon.is-clickable:hover:active{background-color:var(--surface-container)!important;--tw-brightness: brightness(1) !important;--tw-drop-shadow: drop-shadow(0 0 #0000) !important;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}:host ::ng-deep .ui-icon.disabled{cursor:default!important;--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: CacIconComponent, decorators: [{ type: Component, args: [{ selector: 'cac-icon', standalone: true, imports: [MatIconModule, NgStyle], template: "<div\n role=\"presentation\"\n class=\"ui-icon relative {{ disabled ? 'disabled' : '' }} w-full h-full {{ wrapperClass }}\"\n [class.is-clickable]=\"isClickable()\"\n (click)=\"onClickEvent($event)\"\n >\n @if (!!icon) {\n <mat-icon\n #MatIcon\n class=\"{{ iconClass ?? '' }}\"\n [ngStyle]=\"{ width: size, height: size }\"\n [svgIcon]=\"icon\"\n ></mat-icon>\n }\n <ng-content></ng-content>\n</div>\n", styles: [":host{display:inline-block;color:inherit;position:relative;border-radius:.25rem}:host ::ng-deep .ui-icon{height:inherit;width:inherit;color:inherit;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;display:inline-flex;align-items:center;justify-content:center;border-radius:inherit}:host ::ng-deep .ui-icon.is-clickable{cursor:pointer}:host ::ng-deep .ui-icon.is-clickable:hover{background-color:var(--surface-bright)!important;--tw-brightness: brightness(.9) !important;--tw-drop-shadow: drop-shadow(0 1px 1px rgb(0 0 0 / .05)) !important;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}:host ::ng-deep .ui-icon.is-clickable:hover:active{background-color:var(--surface-container)!important;--tw-brightness: brightness(1) !important;--tw-drop-shadow: drop-shadow(0 0 #0000) !important;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}:host ::ng-deep .ui-icon.disabled{cursor:default!important;--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}\n"] }] }], ctorParameters: () => [], propDecorators: { matIcon: [{ type: ViewChild, args: ['MatIcon'] }], icon: [{ type: Input }], disabled: [{ type: Input }], size: [{ type: Input }], strokeWidth: [{ type: Input }], iconClass: [{ type: Input }], wrapperClass: [{ type: Input }], onClick: [{ type: Output }], pointerEventsNone: [{ type: HostBinding, args: ['class.pointer-events-none'] }], thisWidth: [{ type: HostBinding, args: ['style.width'] }], thisHeight: [{ type: HostBinding, args: ['style.height'] }] } }); const BUTTON_COMPONENT_CONFIG = new InjectionToken('CacButtonComponent'); class CacButtonComponent { constructor() { this.iconPosition = 'prefix'; this.appearance = 'filled'; this.tonal = false; this.elevated = false; this.theme = 'primary'; this.disabled = false; this.loadingProp = false; this.iconSize = '1.5rem'; this.padding = '0.5rem 1rem'; this.fitContent = false; this.align = 'center'; // TabIndex this.tab = 0; this.onClick = new EventEmitter(); this.filledClass = true; this.strokedClass = false; this.textClass = false; this.isElevated = false; this.isTonal = false; this.isClicking = false; this.primaryClass = false; this.secondaryClass = false; this.tertiaryClass = false; this.errorClass = false; this.disabledClass = false; this.cursorNotAllowed = false; this.loading = signal(false); this.insufficientPermission = signal(false); componentWithDefaultConfig(this, BUTTON_COMPONENT_CONFIG); toObservable(this.insufficientPermission) .pipe(takeUntilDestroyed()) .subscribe(() => { this.setDisabledClass(); }); } ngOnInit() { this.setTheme(); this.checkPermission(); } ngAfterViewInit() { setTimeout(() => { // Angular... doesn't bind tabIndex in template! this.btnElement.nativeElement.tabIndex = this.tab; }, 5); } ngOnChanges(changes) { if (changes['loading']) { this.loading.set(this.loadingProp ?? false); } if (changes['appearance']) { this.strokedClass = this.appearance === 'stroked'; this.filledClass = this.appearance === 'filled'; this.textClass = this.appearance === 'text'; } if (changes['theme']) { this.setTheme(); } if (changes['elevated']) { this.isElevated = this.elevated; } if (changes['tonal']) { this.isTonal = this.tonal; } this.setDisabledClass(); } onPointerDown() { this.isClicking = true; } onPointerCancel() { this.isClicking = false; } onPointerLeave() { this.isClicking = false; } onPointerUp() { this.isClicking = false; } onClickEvent(e) { if (this.loadingProp || this.disabled || this.insufficientPermission() || this.loading()) return; this.onClick.emit(this.createClickEvent(e)); } createClickEvent(mouseEvent) { const e = { event: mouseEvent ?? new Event('click'), setLoading: (state) => { this.loading.set(state !== undefined ? state : !this.loading()); }, pipe: () => pipe(startWithTap(() => e.setLoading(true)), tap({ next: () => e.setLoading(false), error: () => e.setLoading(false), complete: () => e.setLoading(f