Browse Source

Feature/add copy-to-clipboard functionality to value component (#6575)

* Add copy-to-clipboard functionality

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
pull/5018/merge
Erwin 1 day ago
committed by GitHub
parent
commit
fda0c3afc8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 4
      apps/client/src/styles.scss
  3. 30
      libs/ui/src/lib/value/value.component.html
  4. 6
      libs/ui/src/lib/value/value.component.stories.ts
  5. 58
      libs/ui/src/lib/value/value.component.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added support for a copy-to-clipboard functionality in the value component
### Changed ### Changed
- Improved the language localization for Spanish (`es`) - Improved the language localization for Spanish (`es`)

4
apps/client/src/styles.scss

@ -546,6 +546,10 @@ ngx-skeleton-loader {
padding: 9px 24px !important; padding: 9px 24px !important;
} }
.no-height {
height: unset !important;
}
.no-min-width { .no-min-width {
min-width: unset !important; min-width: unset !important;
} }

30
libs/ui/src/lib/value/value.component.html

@ -4,7 +4,26 @@
</div> </div>
} }
<div class="d-flex flex-column w-100"> <div class="d-flex flex-column w-100">
<ng-template #label><ng-content /></ng-template> <ng-template #label
><span #labelContent>
<ng-content></ng-content>
</span>
</ng-template>
<ng-template #copyIcon>
@if (enableCopyToClipboardButton) {
<button
class="ml-1 no-height no-min-width p-1"
i18n-title
mat-button
title="Copy to clipboard"
type="button"
(click)="onCopyValueToClipboard()"
>
<ion-icon class="text-muted" name="copy-outline" />
</button>
}
</ng-template>
@if (value || value === 0 || value === null) { @if (value || value === 0 || value === null) {
<div <div
class="align-items-center d-flex" class="align-items-center d-flex"
@ -71,6 +90,9 @@
{{ formattedValue }} {{ formattedValue }}
</div> </div>
} }
@if (!hasLabel) {
<ng-container *ngTemplateOutlet="copyIcon" />
}
</div> </div>
} }
@ -88,6 +110,9 @@
@if (size === 'large') { @if (size === 'large') {
<div class="text-truncate"> <div class="text-truncate">
<span class="h6"><ng-container *ngTemplateOutlet="label" /></span> <span class="h6"><ng-container *ngTemplateOutlet="label" /></span>
@if (hasLabel) {
<ng-container *ngTemplateOutlet="copyIcon" />
}
@if (subLabel) { @if (subLabel) {
<span class="text-muted"> {{ subLabel }}</span> <span class="text-muted"> {{ subLabel }}</span>
} }
@ -95,6 +120,9 @@
} @else { } @else {
<small class="d-block text-truncate"> <small class="d-block text-truncate">
<ng-container *ngTemplateOutlet="label" /> <ng-container *ngTemplateOutlet="label" />
@if (hasLabel) {
<ng-container *ngTemplateOutlet="copyIcon" />
}
</small> </small>
} }
</div> </div>

6
libs/ui/src/lib/value/value.component.stories.ts

@ -1,3 +1,4 @@
import '@angular/localize/init';
import { moduleMetadata } from '@storybook/angular'; import { moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -17,6 +18,9 @@ export default {
control: 'select', control: 'select',
options: ['desktop', 'mobile'] options: ['desktop', 'mobile']
}, },
enableCopyToClipboardButton: {
control: 'boolean'
},
size: { size: {
control: 'select', control: 'select',
options: ['small', 'medium', 'large'] options: ['small', 'medium', 'large']
@ -58,7 +62,7 @@ export const Label: Story = {
}, },
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: `<gf-value [locale]="locale" [size]="size" [value]="value">Label</gf-value>` template: `<gf-value [enableCopyToClipboardButton]="enableCopyToClipboardButton" [locale]="locale" [size]="size" [value]="value">Label</gf-value>`
}) })
}; };

58
libs/ui/src/lib/value/value.component.ts

@ -1,30 +1,41 @@
import { getLocale } from '@ghostfolio/common/helper'; import { getLocale } from '@ghostfolio/common/helper';
import { Clipboard } from '@angular/cdk/clipboard';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
CUSTOM_ELEMENTS_SCHEMA, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
ElementRef,
input,
Input, Input,
OnChanges, OnChanges,
computed, ViewChild
input
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatSnackBar } from '@angular/material/snack-bar';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { copyOutline } from 'ionicons/icons';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import ms from 'ms';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, IonIcon, NgxSkeletonLoaderModule], imports: [CommonModule, IonIcon, MatButtonModule, NgxSkeletonLoaderModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-value', selector: 'gf-value',
styleUrls: ['./value.component.scss'], styleUrls: ['./value.component.scss'],
templateUrl: './value.component.html' templateUrl: './value.component.html'
}) })
export class GfValueComponent implements OnChanges { export class GfValueComponent implements AfterViewInit, OnChanges {
@Input() colorizeSign = false; @Input() colorizeSign = false;
@Input() deviceType: string; @Input() deviceType: string;
@Input() enableCopyToClipboardButton = false;
@Input() icon = ''; @Input() icon = '';
@Input() isAbsolute = false; @Input() isAbsolute = false;
@Input() isCurrency = false; @Input() isCurrency = false;
@ -37,12 +48,26 @@ export class GfValueComponent implements OnChanges {
@Input() unit = ''; @Input() unit = '';
@Input() value: number | string = ''; @Input() value: number | string = '';
@ViewChild('labelContent', { static: false })
labelContent!: ElementRef<HTMLSpanElement>;
public absoluteValue = 0; public absoluteValue = 0;
public formattedValue = ''; public formattedValue = '';
public hasLabel = false;
public isNumber = false; public isNumber = false;
public isString = false; public isString = false;
public useAbsoluteValue = false; public useAbsoluteValue = false;
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private clipboard: Clipboard,
private snackBar: MatSnackBar
) {
addIcons({
copyOutline
});
}
public readonly precision = input<number>(); public readonly precision = input<number>();
private readonly formatOptions = computed<Intl.NumberFormatOptions>(() => { private readonly formatOptions = computed<Intl.NumberFormatOptions>(() => {
@ -59,6 +84,17 @@ export class GfValueComponent implements OnChanges {
return precision !== undefined && precision >= 0; return precision !== undefined && precision >= 0;
} }
public ngAfterViewInit() {
if (this.labelContent) {
const element = this.labelContent.nativeElement;
this.hasLabel =
element.children.length > 0 || element.textContent.trim().length > 0;
this.changeDetectorRef.markForCheck();
}
}
public ngOnChanges() { public ngOnChanges() {
this.initializeVariables(); this.initializeVariables();
@ -137,6 +173,18 @@ export class GfValueComponent implements OnChanges {
} }
} }
public onCopyValueToClipboard() {
this.clipboard.copy(String(this.value));
this.snackBar.open(
'✅ ' + $localize`${this.value} has been copied to the clipboard`,
undefined,
{
duration: ms('3 seconds')
}
);
}
private initializeVariables() { private initializeVariables() {
this.absoluteValue = 0; this.absoluteValue = 0;
this.formattedValue = ''; this.formattedValue = '';

Loading…
Cancel
Save