Enhancement: better monetary field with currency code (#5858)

This commit is contained in:
shamoon
2024-02-27 08:26:06 -08:00
committed by GitHub
parent 84721b001f
commit bf11dc8d1b
13 changed files with 283 additions and 38 deletions

View File

@@ -447,7 +447,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">315</context>
<context context-type="linenumber">313</context>
</context-group>
</trans-unit>
<trans-unit id="3768927257183755959" datatype="html">
@@ -506,7 +506,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">307</context>
<context context-type="linenumber">305</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
@@ -632,7 +632,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">324</context>
<context context-type="linenumber">322</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@@ -962,7 +962,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">283</context>
<context context-type="linenumber">281</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -3815,6 +3815,10 @@
<context context-type="sourcefile">src/app/components/common/input/file/file.component.html</context>
<context context-type="linenumber">21</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/monetary/monetary.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/number/number.component.html</context>
<context context-type="linenumber">9</context>
@@ -4824,14 +4828,14 @@
<source>Content</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">190</context>
<context context-type="linenumber">188</context>
</context-group>
</trans-unit>
<trans-unit id="218403386307979629" datatype="html">
<source>Metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">199</context>
<context context-type="linenumber">197</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
@@ -4842,112 +4846,112 @@
<source>Date modified</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">206</context>
<context context-type="linenumber">204</context>
</context-group>
</trans-unit>
<trans-unit id="6392918669949841614" datatype="html">
<source>Date added</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">210</context>
<context context-type="linenumber">208</context>
</context-group>
</trans-unit>
<trans-unit id="146828917013192897" datatype="html">
<source>Media filename</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">214</context>
<context context-type="linenumber">212</context>
</context-group>
</trans-unit>
<trans-unit id="4500855521601039868" datatype="html">
<source>Original filename</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">218</context>
<context context-type="linenumber">216</context>
</context-group>
</trans-unit>
<trans-unit id="7985558498848210210" datatype="html">
<source>Original MD5 checksum</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">222</context>
<context context-type="linenumber">220</context>
</context-group>
</trans-unit>
<trans-unit id="5888243105821763422" datatype="html">
<source>Original file size</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">226</context>
<context context-type="linenumber">224</context>
</context-group>
</trans-unit>
<trans-unit id="2696647325713149563" datatype="html">
<source>Original mime type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">230</context>
<context context-type="linenumber">228</context>
</context-group>
</trans-unit>
<trans-unit id="342875990758166588" datatype="html">
<source>Archive MD5 checksum</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">233</context>
</context-group>
</trans-unit>
<trans-unit id="6033581412811562084" datatype="html">
<source>Archive file size</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">241</context>
<context context-type="linenumber">239</context>
</context-group>
</trans-unit>
<trans-unit id="6992781481378431874" datatype="html">
<source>Original document metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">250</context>
<context context-type="linenumber">248</context>
</context-group>
</trans-unit>
<trans-unit id="2846565152091361585" datatype="html">
<source>Archived document metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">253</context>
<context context-type="linenumber">251</context>
</context-group>
</trans-unit>
<trans-unit id="1295614462098694869" datatype="html">
<source>Preview</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">260</context>
<context context-type="linenumber">258</context>
</context-group>
</trans-unit>
<trans-unit id="7206723502037428235" datatype="html">
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge text-bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">272,275</context>
<context context-type="linenumber">270,273</context>
</context-group>
</trans-unit>
<trans-unit id="5129524307369213584" datatype="html">
<source>Save &amp; next</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">309</context>
<context context-type="linenumber">307</context>
</context-group>
</trans-unit>
<trans-unit id="4910102545766233758" datatype="html">
<source>Save &amp; close</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">312</context>
<context context-type="linenumber">310</context>
</context-group>
</trans-unit>
<trans-unit id="8191371354890763172" datatype="html">
<source>Enter Password</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">363</context>
<context context-type="linenumber">361</context>
</context-group>
</trans-unit>
<trans-unit id="2218903673684131427" datatype="html">

View File

@@ -113,6 +113,7 @@ import { ConfigComponent } from './components/admin/config/config.component'
import { FileComponent } from './components/common/input/file/file.component'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
import {
archive,
arrowCounterclockwise,
@@ -443,6 +444,7 @@ function initializeApp(settings: SettingsService) {
ConfigComponent,
FileComponent,
ConfirmButtonComponent,
MonetaryComponent,
],
imports: [
BrowserModule,

View File

@@ -0,0 +1,27 @@
<div class="mb-3" [class.pb-3]="error">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
@if (title) {
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error">
<span class="input-group-text fw-bold bg-light">{{monetaryValue | currency: currencyCode }}</span>
<input #currencyField class="form-control text-muted mw-60" tabindex="0" [(ngModel)]="currencyCode" maxlength="3" [class.is-invalid]="error" (change)="onChange(value)" [disabled]="disabled">
<input #inputField type="number" tabindex="0" class="form-control text-muted" step=".01" [id]="inputId" [(ngModel)]="monetaryValue" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
</div>
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
.input-group-text {
font-size: inherit;
}
.text-muted:focus-within {
color: var(--bs-body-color) !important;
}
.mw-60 {
max-width: 60px;
}

View File

@@ -0,0 +1,59 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { CurrencyPipe } from '@angular/common'
import { MonetaryComponent } from './monetary.component'
describe('MonetaryComponent', () => {
let component: MonetaryComponent
let fixture: ComponentFixture<MonetaryComponent>
let input: HTMLInputElement
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MonetaryComponent],
providers: [CurrencyPipe],
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
}).compileComponents()
fixture = TestBed.createComponent(MonetaryComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
input = component.inputField.nativeElement
})
it('should set the currency code correctly', () => {
expect(component.currencyCode).toEqual('USD') // default
component.currencyCode = 'EUR'
expect(component.currencyCode).toEqual('EUR')
component.value = 'G123.4'
jest
.spyOn(document, 'activeElement', 'get')
.mockReturnValue(component.currencyField.nativeElement)
expect(component.currencyCode).toEqual('G')
})
it('should parse monetary value only when out of focus', () => {
component.monetaryValue = 10.5
jest.spyOn(document, 'activeElement', 'get').mockReturnValue(null)
expect(component.monetaryValue).toEqual('10.50')
component.value = 'GBP123.4'
jest
.spyOn(document, 'activeElement', 'get')
.mockReturnValue(component.inputField.nativeElement)
expect(component.monetaryValue).toEqual('123.4')
})
it('should report value including currency code and monetary value', () => {
component.currencyCode = 'EUR'
component.monetaryValue = 10.5
expect(component.value).toEqual('EUR10.50')
})
})

View File

@@ -0,0 +1,59 @@
import {
Component,
DEFAULT_CURRENCY_CODE,
ElementRef,
forwardRef,
Inject,
ViewChild,
} from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
@Component({
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MonetaryComponent),
multi: true,
},
],
selector: 'pngx-input-monetary',
templateUrl: './monetary.component.html',
styleUrls: ['./monetary.component.scss'],
})
export class MonetaryComponent extends AbstractInputComponent<string> {
@ViewChild('currencyField')
currencyField: ElementRef
constructor(
@Inject(DEFAULT_CURRENCY_CODE) public defaultCurrencyCode: string
) {
super()
}
get currencyCode(): string {
const focused = document.activeElement === this.currencyField?.nativeElement
if (focused && this.value) return this.value.match(/^([A-Z]{0,3})/)?.[0]
return (
this.value
?.toString()
.toUpperCase()
.match(/^([A-Z]{1,3})/)?.[0] ?? this.defaultCurrencyCode
)
}
set currencyCode(value: string) {
this.value = value + this.monetaryValue?.toString()
}
get monetaryValue(): string {
if (!this.value) return null
const focused = document.activeElement === this.inputField?.nativeElement
const val = parseFloat(this.value.toString().replace(/[^0-9.,]+/g, ''))
return focused ? val.toString() : val.toFixed(2)
}
set monetaryValue(value: number) {
this.value = this.currencyCode + value.toFixed(2)
}
}

View File

@@ -142,14 +142,12 @@
[error]="getCustomFieldError(i)"></pngx-input-number>
}
@case (CustomFieldDataType.Monetary) {
<pngx-input-number formControlName="value"
<pngx-input-monetary formControlName="value"
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
[removable]="userIsOwner"
(removed)="removeField(fieldInstance)"
[horizontal]="true"
[showAdd]="false"
[step]=".01"
[error]="getCustomFieldError(i)"></pngx-input-number>
[error]="getCustomFieldError(i)"></pngx-input-monetary>
}
@case (CustomFieldDataType.Boolean) {
<pngx-input-check formControlName="value"