UI: Improved component and clear code

This commit is contained in:
Vladyslav_Prykhodko 2024-04-22 10:58:58 +03:00
parent 1012b47775
commit ecb68b323d
12 changed files with 98 additions and 61 deletions

View File

@ -43,7 +43,6 @@
"@ngrx/store": "^15.4.0", "@ngrx/store": "^15.4.0",
"@ngrx/store-devtools": "^15.4.0", "@ngrx/store-devtools": "^15.4.0",
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
"@ngx-translate/http-loader": "^7.0.0",
"@svgdotjs/svg.filter.js": "^3.0.8", "@svgdotjs/svg.filter.js": "^3.0.8",
"@svgdotjs/svg.js": "^3.2.0", "@svgdotjs/svg.js": "^3.2.0",
"@tinymce/tinymce-angular": "^7.0.0", "@tinymce/tinymce-angular": "^7.0.0",

View File

@ -31,7 +31,6 @@ import {
TranslateModule, TranslateModule,
TranslateParser TranslateParser
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TbMissingTranslationHandler } from './translate/missing-translate-handler'; import { TbMissingTranslationHandler } from './translate/missing-translate-handler';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule } from '@angular/material/dialog'; import { MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule } from '@angular/material/dialog';
@ -41,9 +40,10 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp
import { WINDOW_PROVIDERS } from '@core/services/window.service'; import { WINDOW_PROVIDERS } from '@core/services/window.service';
import { HotkeyModule } from 'angular2-hotkeys'; import { HotkeyModule } from 'angular2-hotkeys';
import { TranslateDefaultParser } from '@core/translate/translate-default-parser'; import { TranslateDefaultParser } from '@core/translate/translate-default-parser';
import { TranslateDefaultLoader } from '@core/translate/translate-default-loader';
export function HttpLoaderFactory(http: HttpClient) { export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); return new TranslateDefaultLoader(http);
} }
@NgModule({ @NgModule({

View File

@ -34,6 +34,7 @@ import { select, Store } from '@ngrx/store';
import { selectIsAuthenticated } from '@core/auth/auth.selectors'; import { selectIsAuthenticated } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { RequestConfig } from '@core/http/http-utils';
declare const System; declare const System;
@ -106,11 +107,11 @@ export class ResourcesService {
return this.loadResourceByType(fileType, url); return this.loadResourceByType(fileType, url);
} }
public downloadResource(downloadUrl: string): Observable<any> { public downloadResource(downloadUrl: string, config?: RequestConfig): Observable<any> {
return this.http.get(downloadUrl, { return this.http.get(downloadUrl, {...config, ...{
responseType: 'arraybuffer', responseType: 'arraybuffer',
observe: 'response' observe: 'response'
}).pipe( }}).pipe(
map((response) => { map((response) => {
const headers = response.headers; const headers = response.headers;
const filename = headers.get('x-filename'); const filename = headers.get('x-filename');

View File

@ -28,7 +28,6 @@ import { AppState } from '@app/core/core.state';
import { LocalStorageService } from '@app/core/local-storage/local-storage.service'; import { LocalStorageService } from '@app/core/local-storage/local-storage.service';
import { TitleService } from '@app/core/services/title.service'; import { TitleService } from '@app/core/services/title.service';
import { updateUserLang } from '@app/core/settings/settings.utils'; import { updateUserLang } from '@app/core/settings/settings.utils';
import { AuthService } from '@core/auth/auth.service';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions'; import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions';
@ -40,7 +39,6 @@ export class SettingsEffects {
constructor( constructor(
private actions$: Actions<SettingsActions>, private actions$: Actions<SettingsActions>,
private store: Store<AppState>, private store: Store<AppState>,
private authService: AuthService,
private utils: UtilsService, private utils: UtilsService,
private router: Router, private router: Router,
private localStorageService: LocalStorageService, private localStorageService: LocalStorageService,
@ -49,7 +47,6 @@ export class SettingsEffects {
) { ) {
} }
persistSettings = createEffect(() => this.actions$.pipe( persistSettings = createEffect(() => this.actions$.pipe(
ofType( ofType(
SettingsActionTypes.CHANGE_LANGUAGE, SettingsActionTypes.CHANGE_LANGUAGE,
@ -60,7 +57,6 @@ export class SettingsEffects {
) )
), {dispatch: false}); ), {dispatch: false});
setTranslateServiceLanguage = createEffect(() => this.store.pipe( setTranslateServiceLanguage = createEffect(() => this.store.pipe(
select(selectSettingsState), select(selectSettingsState),
map(settings => settings.userLang), map(settings => settings.userLang),
@ -68,7 +64,6 @@ export class SettingsEffects {
tap(userLang => updateUserLang(this.translate, userLang)) tap(userLang => updateUserLang(this.translate, userLang))
), {dispatch: false}); ), {dispatch: false});
setTitle = createEffect(() => merge( setTitle = createEffect(() => merge(
this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)), this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)),
this.router.events.pipe(filter(event => event instanceof ActivationEnd)) this.router.events.pipe(filter(event => event instanceof ActivationEnd))
@ -81,7 +76,6 @@ export class SettingsEffects {
}) })
), {dispatch: false}); ), {dispatch: false});
setPublicId = createEffect(() => merge( setPublicId = createEffect(() => merge(
this.router.events.pipe(filter(event => event instanceof ActivationEnd)) this.router.events.pipe(filter(event => event instanceof ActivationEnd))
).pipe( ).pipe(

View File

@ -18,7 +18,7 @@ import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import * as _moment from 'moment'; import * as _moment from 'moment';
export function updateUserLang(translate: TranslateService, userLang: string) { export function updateUserLang(translate: TranslateService, userLang: string, translations = env.supportedLangs) {
let targetLang = userLang; let targetLang = userLang;
if (!env.production) { if (!env.production) {
console.log(`User lang: ${targetLang}`); console.log(`User lang: ${targetLang}`);
@ -29,7 +29,7 @@ export function updateUserLang(translate: TranslateService, userLang: string) {
console.log(`Fallback to browser lang: ${targetLang}`); console.log(`Fallback to browser lang: ${targetLang}`);
} }
} }
const detectedSupportedLang = detectSupportedLang(targetLang); const detectedSupportedLang = detectSupportedLang(targetLang, translations);
if (!env.production) { if (!env.production) {
console.log(`Detected supported lang: ${detectedSupportedLang}`); console.log(`Detected supported lang: ${detectedSupportedLang}`);
} }
@ -37,10 +37,10 @@ export function updateUserLang(translate: TranslateService, userLang: string) {
_moment.locale([detectedSupportedLang]); _moment.locale([detectedSupportedLang]);
} }
function detectSupportedLang(targetLang: string): string { function detectSupportedLang(targetLang: string, translations: string[]): string {
const langTag = (targetLang || '').split('-').join('_'); const langTag = (targetLang || '').split('-').join('_');
if (langTag.length) { if (langTag.length) {
if (env.supportedLangs.indexOf(langTag) > -1) { if (translations.indexOf(langTag) > -1) {
return langTag; return langTag;
} else { } else {
const parts = langTag.split('_'); const parts = langTag.split('_');
@ -50,7 +50,7 @@ function detectSupportedLang(targetLang: string): string {
} else { } else {
lang = langTag; lang = langTag;
} }
const foundLangs = env.supportedLangs.filter( const foundLangs = translations.filter(
(supportedLang: string) => { (supportedLang: string) => {
const supportedLangParts = supportedLang.split('_'); const supportedLangParts = supportedLang.split('_');
return supportedLangParts[0] === lang; return supportedLangParts[0] === lang;

View File

@ -0,0 +1,14 @@
import { TranslateLoader } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
export class TranslateDefaultLoader implements TranslateLoader {
constructor(private http: HttpClient) {
}
getTranslation(lang: string): Observable<object> {
return this.http.get(`/assets/locale/locale.constant-${lang}.json`);
}
}

View File

@ -338,6 +338,8 @@ export const isEmpty = (a: any): boolean => _.isEmpty(a);
export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path); export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path);
export const setByPath = <T extends object>(object: T, path: string | number | symbol, value: any): T => _.set(object, path, value);
export const isEqualIgnoreUndefined = (a: any, b: any): boolean => { export const isEqualIgnoreUndefined = (a: any, b: any): boolean => {
if (a === b) { if (a === b) {
return true; return true;

View File

@ -46,7 +46,7 @@
<input id="username-input" matInput type="email" autofocus formControlName="username" email required/> <input id="username-input" matInput type="email" autofocus formControlName="username" email required/>
<mat-icon matPrefix>email</mat-icon> <mat-icon matPrefix>email</mat-icon>
<mat-error *ngIf="loginFormGroup.get('username').invalid"> <mat-error *ngIf="loginFormGroup.get('username').invalid">
{{ 'user.invalid-email-format' | translate }} {{ 'login.invalid-email-format' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="tb-appearance-transparent"> <mat-form-field class="tb-appearance-transparent">

View File

@ -18,7 +18,7 @@
<div class="tb-json-content" style="background: #fff;" [ngClass]="{'fill-height': fillHeight}" <div class="tb-json-content" style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen tb-fullscreen
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar"> <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-json-content-toolbar" *ngIf="hideToolbar">
<label class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (!contentValid || required && !contentBody), 'tb-required': !disabled && required}">{{ label }}</label> <label class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (!contentValid || required && !contentBody), 'tb-required': !disabled && required}">{{ label }}</label>
<span fxFlex></span> <span fxFlex></span>
<button type="button" <button type="button"

View File

@ -18,17 +18,19 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter,
forwardRef, forwardRef,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output,
SimpleChanges, SimpleChanges,
ViewChild, ViewEncapsulation ViewChild,
ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormControl, Validator } from '@angular/forms';
import { Ace } from 'ace-builds'; import { Ace } from 'ace-builds';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -38,6 +40,7 @@ import { guid } from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { getAce } from '@shared/models/ace/ace.models'; import { getAce } from '@shared/models/ace/ace.models';
import { beautifyJs } from '@shared/models/beautify.models'; import { beautifyJs } from '@shared/models/beautify.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-json-content', selector: 'tb-json-content',
@ -79,41 +82,30 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
@Input() editorStyle: {[klass: string]: any}; @Input() editorStyle: {[klass: string]: any};
private readonlyValue: boolean; @Input() tbPlaceholder: string;
get readonly(): boolean {
return this.readonlyValue;
}
@Input()
set readonly(value: boolean) {
this.readonlyValue = coerceBooleanProperty(value);
}
private validateContentValue: boolean;
get validateContent(): boolean {
return this.validateContentValue;
}
@Input() @Input()
set validateContent(value: boolean) { @coerceBoolean()
this.validateContentValue = coerceBooleanProperty(value); hideToolbar = false;
}
private validateOnChangeValue: boolean;
get validateOnChange(): boolean {
return this.validateOnChangeValue;
}
@Input() @Input()
set validateOnChange(value: boolean) { @coerceBoolean()
this.validateOnChangeValue = coerceBooleanProperty(value); readonly: boolean;
}
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input() @Input()
set required(value: boolean) { @coerceBoolean()
this.requiredValue = coerceBooleanProperty(value); validateContent: boolean;
}
@Input()
@coerceBoolean()
validateOnChange: boolean;
@Input()
@coerceBoolean()
required: boolean;
@Output()
blur: EventEmitter<void> = new EventEmitter<void>();
fullscreen = false; fullscreen = false;
@ -124,6 +116,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
errorShowed = false; errorShowed = false;
private propagateChange = null; private propagateChange = null;
private onTouched = () => {};
constructor(public elementRef: ElementRef, constructor(public elementRef: ElementRef,
protected store: Store<AppState>, protected store: Store<AppState>,
@ -163,11 +156,17 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
this.updateView(); this.updateView();
} }
}); });
if (this.validateContent) {
this.jsonEditor.on('blur', () => { this.jsonEditor.on('blur', () => {
if (this.validateContent) {
this.contentValid = this.doValidate(true); this.contentValid = this.doValidate(true);
this.cd.markForCheck(); this.cd.markForCheck();
}
this.onTouched();
this.blur.next();
}); });
if (this.tbPlaceholder && this.tbPlaceholder.length) {
this.createPlaceholder();
} }
this.editorResize$ = new ResizeObserver(() => { this.editorResize$ = new ResizeObserver(() => {
this.onAceEditorResize(); this.onAceEditorResize();
@ -177,6 +176,39 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
); );
} }
private createPlaceholder() {
this.jsonEditor.on('input', this.updateEditorPlaceholder.bind(this));
setTimeout(this.updateEditorPlaceholder.bind(this), 100);
}
private updateEditorPlaceholder() {
const shouldShow = !this.jsonEditor.session.getValue().length;
let node: HTMLElement = (this.jsonEditor.renderer as any).emptyMessageNode;
if (!shouldShow && node) {
this.jsonEditor.renderer.getMouseEventTarget().removeChild(node);
(this.jsonEditor.renderer as any).emptyMessageNode = null;
} else if (shouldShow && !node) {
const placeholderElement = $('<textarea></textarea>');
placeholderElement.text(this.tbPlaceholder);
placeholderElement.addClass('ace_invisible ace_emptyMessage');
placeholderElement.css({
padding: '0 9px',
width: '100%',
border: 'none',
textWrap: 'nowrap',
whiteSpace: 'pre',
overflow: 'hidden',
resize: 'none',
fontSize: '15px'
});
const rows = this.tbPlaceholder.split('\n').length;
placeholderElement.attr('rows', rows);
node = placeholderElement[0];
(this.jsonEditor.renderer as any).emptyMessageNode = node;
this.jsonEditor.renderer.getMouseEventTarget().appendChild(node);
}
}
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.editorResize$) { if (this.editorResize$) {
this.editorResize$.disconnect(); this.editorResize$.disconnect();
@ -219,6 +251,7 @@ export class JsonContentComponent implements OnInit, ControlValueAccessor, Valid
} }
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
this.onTouched = fn;
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {

View File

@ -3314,6 +3314,7 @@
"new-password-again": "Confirm new password", "new-password-again": "Confirm new password",
"password-link-sent-message": "Reset link has been sent", "password-link-sent-message": "Reset link has been sent",
"email": "Email", "email": "Email",
"invalid-email-format": "Invalid email format.",
"login-with": "Login with {{name}}", "login-with": "Login with {{name}}",
"or": "or", "or": "or",
"error": "Login error", "error": "Login error",

View File

@ -2572,13 +2572,6 @@
dependencies: dependencies:
tslib "^2.3.0" tslib "^2.3.0"
"@ngx-translate/http-loader@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-7.0.0.tgz#905f38d8d13342621516635bf480ff9a4f73e9fc"
integrity sha512-j+NpXXlcGVdyUNyY/qsJrqqeAdJdizCd+GKh3usXExSqy1aE9866jlAIL+xrfDU4w+LiMoma5pgE4emvFebZmA==
dependencies:
tslib "^2.3.0"
"@nodelib/fs.scandir@2.1.5": "@nodelib/fs.scandir@2.1.5":
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"