mirror of https://github.com/ghostfolio/ghostfolio
				
				
			
							committed by
							
								
								Thomas
							
						
					
				
				 10 changed files with 258 additions and 2 deletions
			
			
		@ -0,0 +1,6 @@ | 
				
			|||||
 | 
					describe('ui', () => { | 
				
			||||
 | 
					  beforeEach(() => cy.visit('/iframe.html?id=valuecomponent--loading')); | 
				
			||||
 | 
					  it('should render the component', () => { | 
				
			||||
 | 
					    cy.get('gf-value').should('exist'); | 
				
			||||
 | 
					  }); | 
				
			||||
 | 
					}); | 
				
			||||
@ -1,7 +1,15 @@ | 
				
			|||||
import { NgModule } from '@angular/core'; | 
					import { NgModule } from '@angular/core'; | 
				
			||||
import { CommonModule } from '@angular/common'; | 
					import { CommonModule } from '@angular/common'; | 
				
			||||
 | 
					import { ValueComponent } from './value/value.component'; | 
				
			||||
 | 
					// import { GfValueModule } from './value/value.module';
 | 
				
			||||
 | 
					
 | 
				
			||||
@NgModule({ | 
					@NgModule({ | 
				
			||||
  imports: [CommonModule] | 
					  imports: [CommonModule/*, GfValueModule*/], | 
				
			||||
 | 
					  declarations: [ | 
				
			||||
 | 
					    ValueComponent | 
				
			||||
 | 
					  ], | 
				
			||||
 | 
					  exports: [ | 
				
			||||
 | 
					    ValueComponent | 
				
			||||
 | 
					  ] | 
				
			||||
}) | 
					}) | 
				
			||||
export class UiModule {} | 
					export class UiModule {} | 
				
			||||
 | 
				
			|||||
@ -0,0 +1,46 @@ | 
				
			|||||
 | 
					<ng-container *ngIf="value || value === 0 || value === null"> | 
				
			||||
 | 
					    <div | 
				
			||||
 | 
					      class="d-flex" | 
				
			||||
 | 
					      [ngClass]="position === 'end' ? 'justify-content-end' : ''" | 
				
			||||
 | 
					    > | 
				
			||||
 | 
					      <ng-container *ngIf="isNumber || value === null"> | 
				
			||||
 | 
					        <div *ngIf="colorizeSign && value > 0" class="mr-1 text-success">+</div> | 
				
			||||
 | 
					        <div *ngIf="colorizeSign && value < 0" class="mr-1 text-danger">-</div> | 
				
			||||
 | 
					        <div *ngIf="isPercent" [ngClass]="size === 'medium' ? 'h4 mb-0' : ''"> | 
				
			||||
 | 
					          {{ formattedValue }}% | 
				
			||||
 | 
					        </div> | 
				
			||||
 | 
					        <div *ngIf="!isPercent" [ngClass]="size === 'medium' ? 'h4 mb-0' : ''"> | 
				
			||||
 | 
					          <ng-container *ngIf="value === null"> | 
				
			||||
 | 
					            <span class="text-monospace text-muted">***</span> | 
				
			||||
 | 
					          </ng-container> | 
				
			||||
 | 
					          <ng-container *ngIf="value !== null"> | 
				
			||||
 | 
					            {{ formattedValue }} | 
				
			||||
 | 
					          </ng-container> | 
				
			||||
 | 
					        </div> | 
				
			||||
 | 
					        <small *ngIf="currency && size === 'medium'" class="ml-1"> | 
				
			||||
 | 
					          {{ currency }} | 
				
			||||
 | 
					        </small> | 
				
			||||
 | 
					        <div *ngIf="currency && size !== 'medium'" class="ml-1"> | 
				
			||||
 | 
					          {{ currency }} | 
				
			||||
 | 
					        </div> | 
				
			||||
 | 
					      </ng-container> | 
				
			||||
 | 
					      <ng-container *ngIf="isDate"> | 
				
			||||
 | 
					        <div [ngClass]="size === 'medium' ? 'h4 mb-0' : ''"> | 
				
			||||
 | 
					          {{ formattedDate }} | 
				
			||||
 | 
					        </div> | 
				
			||||
 | 
					      </ng-container> | 
				
			||||
 | 
					    </div> | 
				
			||||
 | 
					    <small *ngIf="label"> | 
				
			||||
 | 
					      {{ label }} | 
				
			||||
 | 
					    </small> | 
				
			||||
 | 
					  </ng-container> | 
				
			||||
 | 
					   | 
				
			||||
 | 
					  <ngx-skeleton-loader | 
				
			||||
 | 
					    *ngIf="value === undefined" | 
				
			||||
 | 
					    animation="pulse" | 
				
			||||
 | 
					    [theme]="{ | 
				
			||||
 | 
					      height: '1.5rem', | 
				
			||||
 | 
					      width: '5rem' | 
				
			||||
 | 
					    }" | 
				
			||||
 | 
					  ></ngx-skeleton-loader> | 
				
			||||
 | 
					   | 
				
			||||
@ -0,0 +1,5 @@ | 
				
			|||||
 | 
					:host { | 
				
			||||
 | 
					  display: flex; | 
				
			||||
 | 
					  flex-direction: column; | 
				
			||||
 | 
					  font-variant-numeric: tabular-nums; | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,25 @@ | 
				
			|||||
 | 
					import { ComponentFixture, TestBed } from '@angular/core/testing'; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					import { ValueComponent } from './value.component'; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					describe('ValueComponent', () => { | 
				
			||||
 | 
					  let component: ValueComponent; | 
				
			||||
 | 
					  let fixture: ComponentFixture<ValueComponent>; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  beforeEach(async () => { | 
				
			||||
 | 
					    await TestBed.configureTestingModule({ | 
				
			||||
 | 
					      declarations: [ ValueComponent ] | 
				
			||||
 | 
					    }) | 
				
			||||
 | 
					    .compileComponents(); | 
				
			||||
 | 
					  }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  beforeEach(() => { | 
				
			||||
 | 
					    fixture = TestBed.createComponent(ValueComponent); | 
				
			||||
 | 
					    component = fixture.componentInstance; | 
				
			||||
 | 
					    fixture.detectChanges(); | 
				
			||||
 | 
					  }); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  it('should create', () => { | 
				
			||||
 | 
					    expect(component).toBeTruthy(); | 
				
			||||
 | 
					  }); | 
				
			||||
 | 
					}); | 
				
			||||
@ -0,0 +1,38 @@ | 
				
			|||||
 | 
					import { moduleMetadata, Story, Meta } from '@storybook/angular'; | 
				
			||||
 | 
					import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; | 
				
			||||
 | 
					import { ValueComponent } from './value.component'; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export default { | 
				
			||||
 | 
					  title: 'Value', | 
				
			||||
 | 
					  component: ValueComponent, | 
				
			||||
 | 
					  decorators: [ | 
				
			||||
 | 
					    moduleMetadata({ | 
				
			||||
 | 
					      imports: [NgxSkeletonLoaderModule], | 
				
			||||
 | 
					    }) | 
				
			||||
 | 
					  ], | 
				
			||||
 | 
					} as Meta<ValueComponent>; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					const Template: Story<ValueComponent> = (args: ValueComponent) => ({ | 
				
			||||
 | 
					  props: args, | 
				
			||||
 | 
					}); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export const Loading = Template.bind({}); | 
				
			||||
 | 
					Loading.args = { | 
				
			||||
 | 
					  value: undefined | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export const Integer = Template.bind({}); | 
				
			||||
 | 
					Integer.args = { | 
				
			||||
 | 
					  isInteger: true, | 
				
			||||
 | 
					  locale: 'en', | 
				
			||||
 | 
					  value: 7 | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					export const Currency = Template.bind({}); | 
				
			||||
 | 
					Currency.args = { | 
				
			||||
 | 
					  currency: 'USD', | 
				
			||||
 | 
					  isInteger: true, | 
				
			||||
 | 
					  label: 'Label', | 
				
			||||
 | 
					  locale: 'en', | 
				
			||||
 | 
					  value: 7 | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,108 @@ | 
				
			|||||
 | 
					import { | 
				
			||||
 | 
					  ChangeDetectionStrategy, | 
				
			||||
 | 
					  Component, | 
				
			||||
 | 
					  Input, | 
				
			||||
 | 
					  OnChanges, | 
				
			||||
 | 
					} from '@angular/core'; | 
				
			||||
 | 
					import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; | 
				
			||||
 | 
					import { format, isDate } from 'date-fns'; | 
				
			||||
 | 
					import { isNumber } from 'lodash'; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					@Component({ | 
				
			||||
 | 
					  selector: 'gf-value', | 
				
			||||
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush, | 
				
			||||
 | 
					  templateUrl: './value.component.html', | 
				
			||||
 | 
					  styleUrls: ['./value.component.scss'] | 
				
			||||
 | 
					}) | 
				
			||||
 | 
					export class ValueComponent implements OnChanges { | 
				
			||||
 | 
					  @Input() colorizeSign = false; | 
				
			||||
 | 
					  @Input() currency = ''; | 
				
			||||
 | 
					  @Input() isCurrency = false; | 
				
			||||
 | 
					  @Input() isInteger = false; | 
				
			||||
 | 
					  @Input() isPercent = false; | 
				
			||||
 | 
					  @Input() label = ''; | 
				
			||||
 | 
					  @Input() locale = ''; | 
				
			||||
 | 
					  @Input() position = ''; | 
				
			||||
 | 
					  @Input() size = ''; | 
				
			||||
 | 
					  @Input() value: number | string = ''; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  public absoluteValue = 0; | 
				
			||||
 | 
					  public formattedDate = ''; | 
				
			||||
 | 
					  public formattedValue = ''; | 
				
			||||
 | 
					  public isDate = false; | 
				
			||||
 | 
					  public isNumber = false; | 
				
			||||
 | 
					  public useAbsoluteValue = false; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  public constructor() {} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					  public ngOnChanges() { | 
				
			||||
 | 
					    if (this.value || this.value === 0) { | 
				
			||||
 | 
					      if (isNumber(this.value)) { | 
				
			||||
 | 
					        this.isDate = false; | 
				
			||||
 | 
					        this.isNumber = true; | 
				
			||||
 | 
					        this.absoluteValue = Math.abs(<number>this.value); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        if (this.colorizeSign) { | 
				
			||||
 | 
					          this.useAbsoluteValue = true; | 
				
			||||
 | 
					          if (this.currency || this.isCurrency) { | 
				
			||||
 | 
					            try { | 
				
			||||
 | 
					              this.formattedValue = this.absoluteValue.toLocaleString( | 
				
			||||
 | 
					                this.locale, | 
				
			||||
 | 
					                { | 
				
			||||
 | 
					                  maximumFractionDigits: 2, | 
				
			||||
 | 
					                  minimumFractionDigits: 2 | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					              ); | 
				
			||||
 | 
					            } catch {} | 
				
			||||
 | 
					          } else if (this.isPercent) { | 
				
			||||
 | 
					            try { | 
				
			||||
 | 
					              this.formattedValue = (this.absoluteValue * 100).toLocaleString( | 
				
			||||
 | 
					                this.locale, | 
				
			||||
 | 
					                { | 
				
			||||
 | 
					                  maximumFractionDigits: 2, | 
				
			||||
 | 
					                  minimumFractionDigits: 2 | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					              ); | 
				
			||||
 | 
					            } catch {} | 
				
			||||
 | 
					          } | 
				
			||||
 | 
					        } else if (this.isPercent) { | 
				
			||||
 | 
					          try { | 
				
			||||
 | 
					            this.formattedValue = (this.value * 100).toLocaleString( | 
				
			||||
 | 
					              this.locale, | 
				
			||||
 | 
					              { | 
				
			||||
 | 
					                maximumFractionDigits: 2, | 
				
			||||
 | 
					                minimumFractionDigits: 2 | 
				
			||||
 | 
					              } | 
				
			||||
 | 
					            ); | 
				
			||||
 | 
					          } catch {} | 
				
			||||
 | 
					        } else if (this.currency || this.isCurrency) { | 
				
			||||
 | 
					          try { | 
				
			||||
 | 
					            this.formattedValue = this.value?.toLocaleString(this.locale, { | 
				
			||||
 | 
					              maximumFractionDigits: 2, | 
				
			||||
 | 
					              minimumFractionDigits: 2 | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					          } catch {} | 
				
			||||
 | 
					        } else if (this.isInteger) { | 
				
			||||
 | 
					          try { | 
				
			||||
 | 
					            this.formattedValue = this.value?.toLocaleString(this.locale, { | 
				
			||||
 | 
					              maximumFractionDigits: 0, | 
				
			||||
 | 
					              minimumFractionDigits: 0 | 
				
			||||
 | 
					            }); | 
				
			||||
 | 
					          } catch {} | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					      } else { | 
				
			||||
 | 
					        try { | 
				
			||||
 | 
					          if (isDate(new Date(this.value))) { | 
				
			||||
 | 
					            this.isDate = true; | 
				
			||||
 | 
					            this.isNumber = false; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            this.formattedDate = format( | 
				
			||||
 | 
					              new Date(<string>this.value), | 
				
			||||
 | 
					              DEFAULT_DATE_FORMAT | 
				
			||||
 | 
					            ); | 
				
			||||
 | 
					          } | 
				
			||||
 | 
					        } catch {} | 
				
			||||
 | 
					      } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,13 @@ | 
				
			|||||
 | 
					import { CommonModule } from '@angular/common'; | 
				
			||||
 | 
					import { NgModule } from '@angular/core'; | 
				
			||||
 | 
					import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					import { ValueComponent } from './value.component'; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					@NgModule({ | 
				
			||||
 | 
					  declarations: [ValueComponent], | 
				
			||||
 | 
					  exports: [ValueComponent], | 
				
			||||
 | 
					  imports: [CommonModule, NgxSkeletonLoaderModule], | 
				
			||||
 | 
					  providers: [] | 
				
			||||
 | 
					}) | 
				
			||||
 | 
					export class GfValueModule {} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue