Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43d5dcf1d3 | |||
| 2dd05241fc | |||
| 74b30e5427 | |||
| 81ee7855a7 | |||
| cb82769f31 | |||
| 4a33c9ade5 | |||
| c6ca5eb6ab | |||
| dde8579fbc | |||
| 23f2c5404f | |||
| 499f8ac075 |
@@ -7,13 +7,13 @@
|
|||||||
|
|
||||||
# Redirige solo el scope @pi al repositorio npm (hosted) de Nexus.
|
# Redirige solo el scope @pi al repositorio npm (hosted) de Nexus.
|
||||||
# Cambia la URL por la de tu instancia y el nombre de tu repo npm-hosted.
|
# Cambia la URL por la de tu instancia y el nombre de tu repo npm-hosted.
|
||||||
@pi:registry=https://nexus.pireon.dev/repository/npm-private/
|
@pi:registry=https://nexus.pireon.dev/repository/npm-releases/
|
||||||
|
|
||||||
# Autenticación por token (recomendado, funciona igual en local y en CI).
|
# Autenticación por token (recomendado, funciona igual en local y en CI).
|
||||||
# El token se genera en Nexus: usuario > Security > NuGet/npm token, o vía
|
# El token se genera en Nexus: usuario > Security > NuGet/npm token, o vía
|
||||||
# `npm login --registry=https://nexus.pireon.dev/repository/npm-private/`
|
# `npm login --registry=https://nexus.pireon.dev/repository/npm-releases/`
|
||||||
# (esto último escribe la línea de abajo automáticamente en tu ~/.npmrc).
|
# (esto último escribe la línea de abajo automáticamente en tu ~/.npmrc).
|
||||||
//nexus.pireon.dev/repository/npm-private/:_authToken=${NEXUS_NPM_TOKEN}
|
//nexus.pireon.dev/repository/npm-releases/:_authToken=${NEXUS_NPM_TOKEN}
|
||||||
|
|
||||||
# Nexus no es el registro público de npm: sin este flag, `npm publish`
|
# Nexus no es el registro público de npm: sin este flag, `npm publish`
|
||||||
# rechaza paquetes con scope si no lleva --access, y con un registro privado
|
# rechaza paquetes con scope si no lleva --access, y con un registro privado
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ hecho **no se pueden enlazar a mano** esas entradas en un elemento que ya use
|
|||||||
`[formField]`: Angular lo rechaza con `NG8022`. Los validadores van en el
|
`[formField]`: Angular lo rechaza con `NG8022`. Los validadores van en el
|
||||||
`form()`, no en la plantilla.
|
`form()`, no en la plantilla.
|
||||||
|
|
||||||
|
Los errores se ven cuando el campo está **tocado**: al salir de él, o al
|
||||||
|
enviar con `submit()` de Signal Forms, que marca todo el formulario. Un campo
|
||||||
|
obligatorio vacío no sale en rojo nada más abrir el formulario. Para volver a
|
||||||
|
abrir un formulario como nuevo, `f().reset(valor)` pone los valores y quita el
|
||||||
|
estado de tocado.
|
||||||
|
|
||||||
Se puede ver funcionando en la app `playground` (`nx serve playground`).
|
Se puede ver funcionando en la app `playground` (`nx serve playground`).
|
||||||
|
|
||||||
## Theming: tokens CSS
|
## Theming: tokens CSS
|
||||||
@@ -184,11 +190,23 @@ Detalles a tener en cuenta:
|
|||||||
- **`Dataset.cubicInterpolationMode` pasa a opcional.** Solo tiene sentido en
|
- **`Dataset.cubicInterpolationMode` pasa a opcional.** Solo tiene sentido en
|
||||||
`line`; forzarlo en `bar`/`pie`/`doughnut` no aportaba nada y esos tipos ni
|
`line`; forzarlo en `bar`/`pie`/`doughnut` no aportaba nada y esos tipos ni
|
||||||
siquiera usan `Dataset` (los dos últimos usan `ChartSlice`).
|
siquiera usan `Dataset` (los dos últimos usan `ChartSlice`).
|
||||||
|
- **Cada gráfico registra lo suyo en `buildChart()`, no al importar.** El
|
||||||
|
paquete es un único `fesm`, así que un `Chart.register(...)` a nivel de
|
||||||
|
módulo se ejecutaba en cualquier app que importase `@pi/ui` y metía Chart.js
|
||||||
|
entero en su bundle inicial, pintase gráficos o no (en TaskStream, unos
|
||||||
|
230 kB). Registrar dos veces lo mismo no hace nada. Si se añade un tipo de
|
||||||
|
gráfico, su registro va ahí también.
|
||||||
- **El `type="bar"` del `pi-chart` original nunca funcionó**: el input
|
- **El `type="bar"` del `pi-chart` original nunca funcionó**: el input
|
||||||
existía pero `getLineChartConfigs()` construía siempre un chart `line`
|
existía pero `getLineChartConfigs()` construía siempre un chart `line`
|
||||||
ignorando su valor. Se descubrió al dividir los componentes; `pi-bar-chart`
|
ignorando su valor. Se descubrió al dividir los componentes; `pi-bar-chart`
|
||||||
es la primera versión que realmente renderiza barras.
|
es la primera versión que realmente renderiza barras.
|
||||||
|
|
||||||
|
**Sin lodash.** Las tres funciones que se usaban (`isEqual`, `some` con un
|
||||||
|
objeto como criterio y `capitalize`) están en `util/common/compare.ts`, con la
|
||||||
|
misma semántica. lodash es CommonJS y sin mapa de `exports`: entero pesaba en el
|
||||||
|
bundle de quien usara un select, y por funciones (`lodash/isEqual`) Node en ESM
|
||||||
|
(Vitest, SSR) no lo resolvía.
|
||||||
|
|
||||||
**El loader de servidor de traducciones ya no está en el barrel.**
|
**El loader de servidor de traducciones ya no está en el barrel.**
|
||||||
`util/translate/loaders/translate-server.loader` usa `fs`, `path` y `process`;
|
`util/translate/loaders/translate-server.loader` usa `fs`, `path` y `process`;
|
||||||
exportarlo desde `@pi/ui` rompía el bundle de navegador de cualquier app que
|
exportarlo desde `@pi/ui` rompía el bundle de navegador de cualquier app que
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ siempre el *fallback* de `var()` (el valor que tenían antes de tokenizar).
|
|||||||
| `--pi-theme-accordion-radius` | `var(--pi-border-radius-large)` | `var(--pi-border-radius-xx-small)` | `accordion.component.scss`, `_horizontal-accordion.scss`, `_vertical-accordion.scss` (×6, incluye un `calc(... - 0.0625rem)`) |
|
| `--pi-theme-accordion-radius` | `var(--pi-border-radius-large)` | `var(--pi-border-radius-xx-small)` | `accordion.component.scss`, `_horizontal-accordion.scss`, `_vertical-accordion.scss` (×6, incluye un `calc(... - 0.0625rem)`) |
|
||||||
| `--pi-theme-overlay-divider-color` | `var(--pi-color-grey-100)` | — | `drawer.component.scss`, `hub.component.scss` (borde bajo la cabecera) |
|
| `--pi-theme-overlay-divider-color` | `var(--pi-color-grey-100)` | — | `drawer.component.scss`, `hub.component.scss` (borde bajo la cabecera) |
|
||||||
| `--pi-theme-hub-grabber-color` | `var(--pi-color-grey-100)` | — | `hub.component.scss` (el asa superior) |
|
| `--pi-theme-hub-grabber-color` | `var(--pi-color-grey-100)` | — | `hub.component.scss` (el asa superior) |
|
||||||
|
| `--pi-theme-checkbox-border-color` | `var(--pi-color-grey-400)` | — | `_mat-checkbox.scss` (borde de la casilla sin marcar; necesita 3:1 contra el fondo para verse) |
|
||||||
|
|
||||||
### Puente con Angular Material (`--mat-sys-*`)
|
### Puente con Angular Material (`--mat-sys-*`)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"lib": {
|
"lib": {
|
||||||
"entryFile": "src/index.ts"
|
"entryFile": "src/index.ts"
|
||||||
},
|
},
|
||||||
"allowedNonPeerDependencies": ["lodash", "luxon", "ngx-infinite-scroll"],
|
"allowedNonPeerDependencies": ["luxon", "ngx-infinite-scroll"],
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*.scss",
|
"glob": "**/*.scss",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@pi/ui",
|
"name": "@pi/ui",
|
||||||
"version": "0.0.2",
|
"version": "0.0.9",
|
||||||
"description": "Librería de componentes Angular standalone (botones, campos con Signal Forms, cards, charts, drawer/hub/modal, theming vía tokens CSS...).",
|
"description": "Librería de componentes Angular standalone (botones, campos con Signal Forms, cards, charts, drawer/hub/modal, theming vía tokens CSS...).",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"generators": "./generators.json",
|
"generators": "./generators.json",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"registry": "https://nexus.pireon.dev/repository/npm-private/",
|
"registry": "https://nexus.pireon.dev/repository/npm-releases/",
|
||||||
"access": "restricted"
|
"access": "restricted"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
"chart.js": "^4.0.0"
|
"chart.js": "^4.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "^4.17.0",
|
|
||||||
"luxon": "^3.0.0",
|
"luxon": "^3.0.0",
|
||||||
"ngx-infinite-scroll": "^22.0.0"
|
"ngx-infinite-scroll": "^22.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -280,8 +280,10 @@
|
|||||||
// Los `<button>` internos de Angular Material quedan fuera: el calendario del
|
// Los `<button>` internos de Angular Material quedan fuera: el calendario del
|
||||||
// datepicker, por ejemplo, monta cada día como un `<button>` y el
|
// datepicker, por ejemplo, monta cada día como un `<button>` y el
|
||||||
// `min-width: 5.5rem` de la librería lo sacaba fuera del panel. La exclusión
|
// `min-width: 5.5rem` de la librería lo sacaba fuera del panel. La exclusión
|
||||||
// va en `:where()` para no alterar la especificidad de la regla.
|
// va en `:where()` para no alterar la especificidad de la regla. La zona de
|
||||||
button:not(:where([class*='mat-'])) {
|
// `pi-file-upload` también es un `<button>` (para el teclado), pero con su
|
||||||
|
// propio aspecto: sin excluirla, el alto y el relleno de botón la aplastaban.
|
||||||
|
button:not(:where([class*='mat-'], .pi-file-upload__dropzone)) {
|
||||||
// Colors
|
// Colors
|
||||||
@include themeButtonsColors;
|
@include themeButtonsColors;
|
||||||
// Sizes
|
// Sizes
|
||||||
@@ -293,8 +295,10 @@
|
|||||||
// Los `<button>` internos de Angular Material quedan fuera: el calendario del
|
// Los `<button>` internos de Angular Material quedan fuera: el calendario del
|
||||||
// datepicker, por ejemplo, monta cada día como un `<button>` y el
|
// datepicker, por ejemplo, monta cada día como un `<button>` y el
|
||||||
// `min-width: 5.5rem` de la librería lo sacaba fuera del panel. La exclusión
|
// `min-width: 5.5rem` de la librería lo sacaba fuera del panel. La exclusión
|
||||||
// va en `:where()` para no alterar la especificidad de la regla.
|
// va en `:where()` para no alterar la especificidad de la regla. La zona de
|
||||||
button:not(:where([class*='mat-'])) {
|
// `pi-file-upload` también es un `<button>` (para el teclado), pero con su
|
||||||
|
// propio aspecto: sin excluirla, el alto y el relleno de botón la aplastaban.
|
||||||
|
button:not(:where([class*='mat-'], .pi-file-upload__dropzone)) {
|
||||||
// Colors
|
// Colors
|
||||||
@include themeButtonsColors;
|
@include themeButtonsColors;
|
||||||
// Sizes
|
// Sizes
|
||||||
|
|||||||
@@ -145,8 +145,10 @@ html,
|
|||||||
// Los `<button>` internos de Angular Material quedan fuera: el calendario del
|
// Los `<button>` internos de Angular Material quedan fuera: el calendario del
|
||||||
// datepicker, por ejemplo, monta cada día como un `<button>` y el
|
// datepicker, por ejemplo, monta cada día como un `<button>` y el
|
||||||
// `min-width: 5.5rem` de la librería lo sacaba fuera del panel. La exclusión
|
// `min-width: 5.5rem` de la librería lo sacaba fuera del panel. La exclusión
|
||||||
// va en `:where()` para no alterar la especificidad de la regla.
|
// va en `:where()` para no alterar la especificidad de la regla. La zona de
|
||||||
button:not(:where([class*='mat-'])) {
|
// `pi-file-upload` también es un `<button>` (para el teclado), pero con su
|
||||||
|
// propio aspecto: sin excluirla, el alto y el relleno de botón la aplastaban.
|
||||||
|
button:not(:where([class*='mat-'], .pi-file-upload__dropzone)) {
|
||||||
// Default properties
|
// Default properties
|
||||||
@include defaultButtonsProperties;
|
@include defaultButtonsProperties;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
&:enabled {
|
&:enabled {
|
||||||
&:not(:checked):not(:indeterminate):not([data-indeterminate='true']) {
|
&:not(:checked):not(:indeterminate):not([data-indeterminate='true']) {
|
||||||
& ~ .mdc-checkbox__background {
|
& ~ .mdc-checkbox__background {
|
||||||
border-color: var(--pi-color-#{$color}) !important;
|
border-color: var(
|
||||||
|
--pi-theme-checkbox-border-color,
|
||||||
|
var(--pi-color-#{$color})
|
||||||
|
) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +21,9 @@
|
|||||||
.pi-select-option {
|
.pi-select-option {
|
||||||
@include checkboxColor(primary-200);
|
@include checkboxColor(primary-200);
|
||||||
}
|
}
|
||||||
@include checkboxColor(grey-100);
|
// grey-400: an empty box needs 3:1 against the page to be seen at all
|
||||||
|
// (WCAG 1.4.11); grey-100 gave 1.2:1 and the box vanished on white.
|
||||||
|
@include checkboxColor(grey-400);
|
||||||
|
|
||||||
// Checkbox
|
// Checkbox
|
||||||
.mdc-checkbox__background {
|
.mdc-checkbox__background {
|
||||||
@@ -136,7 +141,10 @@
|
|||||||
&:checked {
|
&:checked {
|
||||||
& ~ .mdc-checkbox__background {
|
& ~ .mdc-checkbox__background {
|
||||||
border-color: var(--pi-color-primary);
|
border-color: var(--pi-color-primary);
|
||||||
background-color: var(--pi-color-white);
|
background-color: var(
|
||||||
|
--pi-theme-field-surface,
|
||||||
|
var(--pi-color-white)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& ~ .mdc-checkbox__background {
|
& ~ .mdc-checkbox__background {
|
||||||
@@ -163,7 +171,10 @@
|
|||||||
&:checked {
|
&:checked {
|
||||||
& ~ .mdc-checkbox__background {
|
& ~ .mdc-checkbox__background {
|
||||||
border-color: var(--pi-color-primary);
|
border-color: var(--pi-color-primary);
|
||||||
background-color: var(--pi-color-white);
|
background-color: var(
|
||||||
|
--pi-theme-field-surface,
|
||||||
|
var(--pi-color-white)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `pi-date-range-field`: sus dos fechas no son `.mat-mdc-input-element`,
|
||||||
|
// así que la regla de arriba no las alcanza y se quedaban pegadas arriba
|
||||||
|
// de la caja. Ocupan toda su altura y se centran como un input normal.
|
||||||
|
.mat-date-range-input,
|
||||||
|
.mat-date-range-input-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&.mdc-text-field--disabled {
|
&.mdc-text-field--disabled {
|
||||||
.mat-mdc-input-element,
|
.mat-mdc-input-element,
|
||||||
.mdc-notched-outline,
|
.mdc-notched-outline,
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import { barDatasetConfig } from '../chart.configs';
|
|||||||
import { PiBaseCartesianChartComponent } from '../base-cartesian-chart.component';
|
import { PiBaseCartesianChartComponent } from '../base-cartesian-chart.component';
|
||||||
import { PiChartLegendComponent } from '../chart-legend/chart-legend.component';
|
import { PiChartLegendComponent } from '../chart-legend/chart-legend.component';
|
||||||
|
|
||||||
Chart.register(CategoryScale, LinearScale, BarController, BarElement, Tooltip, Legend);
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pi-bar-chart',
|
selector: 'pi-bar-chart',
|
||||||
templateUrl: './bar-chart.component.html',
|
templateUrl: './bar-chart.component.html',
|
||||||
@@ -30,6 +28,9 @@ export class PiBarChartComponent extends PiBaseCartesianChartComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected buildChart(): Chart {
|
protected buildChart(): Chart {
|
||||||
|
// Aquí y no al importar: así una app que nunca pinta este gráfico no
|
||||||
|
// arrastra Chart.js. Registrar dos veces no hace nada.
|
||||||
|
Chart.register(CategoryScale, LinearScale, BarController, BarElement, Tooltip, Legend);
|
||||||
return new Chart(this.chartRef().nativeElement, {
|
return new Chart(this.chartRef().nativeElement, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import { Device } from '../../util/device-configs';
|
|||||||
* `pi-pie-chart`, `pi-doughnut-chart`): el `<canvas>`, el ciclo de vida que
|
* `pi-pie-chart`, `pi-doughnut-chart`): el `<canvas>`, el ciclo de vida que
|
||||||
* construye el `Chart` una vez la vista está lista, y el `TranslateService`
|
* construye el `Chart` una vez la vista está lista, y el `TranslateService`
|
||||||
* que todos usan para las etiquetas. Cada tipo registra solo los
|
* que todos usan para las etiquetas. Cada tipo registra solo los
|
||||||
* controladores/elementos de Chart.js que necesita — no hay un registro
|
* controladores/elementos de Chart.js que necesita, y lo hace en
|
||||||
* global compartido, así que un consumidor que solo importe `pi-pie-chart`
|
* `buildChart`, no al importar el módulo: el paquete es un único `fesm`, así
|
||||||
* no arrastra el código de líneas/barras.
|
* que un `Chart.register` a nivel de módulo metería Chart.js entero en el
|
||||||
|
* bundle inicial de cualquier app que use `@pi/ui`, pinte gráficos o no.
|
||||||
*/
|
*/
|
||||||
@Component({ template: '' })
|
@Component({ template: '' })
|
||||||
export abstract class PiBaseChartComponent implements AfterViewInit {
|
export abstract class PiBaseChartComponent implements AfterViewInit {
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import {
|
|||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { PiBaseArcChartComponent } from '../base-arc-chart.component';
|
import { PiBaseArcChartComponent } from '../base-arc-chart.component';
|
||||||
|
|
||||||
Chart.register(ArcElement, DoughnutController, Tooltip, Legend);
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pi-doughnut-chart',
|
selector: 'pi-doughnut-chart',
|
||||||
templateUrl: './doughnut-chart.component.html',
|
templateUrl: './doughnut-chart.component.html',
|
||||||
@@ -18,6 +16,9 @@ Chart.register(ArcElement, DoughnutController, Tooltip, Legend);
|
|||||||
})
|
})
|
||||||
export class PiDoughnutChartComponent extends PiBaseArcChartComponent {
|
export class PiDoughnutChartComponent extends PiBaseArcChartComponent {
|
||||||
protected buildChart(): Chart {
|
protected buildChart(): Chart {
|
||||||
|
// Aquí y no al importar: así una app que nunca pinta este gráfico no
|
||||||
|
// arrastra Chart.js. Registrar dos veces no hace nada.
|
||||||
|
Chart.register(ArcElement, DoughnutController, Tooltip, Legend);
|
||||||
return new Chart(this.chartRef().nativeElement, {
|
return new Chart(this.chartRef().nativeElement, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
data: this.getArcData<'doughnut'>(),
|
data: this.getArcData<'doughnut'>(),
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import { ArcElement, Chart, DoughnutController } from 'chart.js';
|
|||||||
import { DatasetColor } from '../../../util/models';
|
import { DatasetColor } from '../../../util/models';
|
||||||
import { PiBaseChartComponent } from '../base-chart.component';
|
import { PiBaseChartComponent } from '../base-chart.component';
|
||||||
|
|
||||||
Chart.register(ArcElement, DoughnutController);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Un único valor sobre `max`, no una serie ni porciones — por eso extiende
|
* Un único valor sobre `max`, no una serie ni porciones — por eso extiende
|
||||||
* directamente `PiBaseChartComponent` (la base raíz) en vez de
|
* directamente `PiBaseChartComponent` (la base raíz) en vez de
|
||||||
@@ -36,6 +34,9 @@ export class PiGaugeChartComponent extends PiBaseChartComponent {
|
|||||||
);
|
);
|
||||||
|
|
||||||
protected buildChart(): Chart {
|
protected buildChart(): Chart {
|
||||||
|
// Aquí y no al importar: así una app que nunca pinta este gráfico no
|
||||||
|
// arrastra Chart.js. Registrar dos veces no hace nada.
|
||||||
|
Chart.register(ArcElement, DoughnutController);
|
||||||
const clamped = this.clampedValue();
|
const clamped = this.clampedValue();
|
||||||
return new Chart(this.chartRef().nativeElement, {
|
return new Chart(this.chartRef().nativeElement, {
|
||||||
type: 'doughnut',
|
type: 'doughnut',
|
||||||
|
|||||||
@@ -14,17 +14,6 @@ import { hexToRgba, lineDatasetConfig } from '../chart.configs';
|
|||||||
import { PiBaseCartesianChartComponent } from '../base-cartesian-chart.component';
|
import { PiBaseCartesianChartComponent } from '../base-cartesian-chart.component';
|
||||||
import { PiChartLegendComponent } from '../chart-legend/chart-legend.component';
|
import { PiChartLegendComponent } from '../chart-legend/chart-legend.component';
|
||||||
|
|
||||||
Chart.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
LineController,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
Filler,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
);
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pi-line-chart',
|
selector: 'pi-line-chart',
|
||||||
templateUrl: './line-chart.component.html',
|
templateUrl: './line-chart.component.html',
|
||||||
@@ -61,6 +50,9 @@ export class PiLineChartComponent extends PiBaseCartesianChartComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected buildChart(): Chart {
|
protected buildChart(): Chart {
|
||||||
|
// Aquí y no al importar: así una app que nunca pinta este gráfico no
|
||||||
|
// arrastra Chart.js. Registrar dos veces no hace nada.
|
||||||
|
Chart.register(CategoryScale, LinearScale, LineController, LineElement, PointElement, Filler, Tooltip, Legend);
|
||||||
return new Chart(this.chartRef().nativeElement, {
|
return new Chart(this.chartRef().nativeElement, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|||||||
import { ArcElement, Chart, Legend, PieController, Tooltip } from 'chart.js';
|
import { ArcElement, Chart, Legend, PieController, Tooltip } from 'chart.js';
|
||||||
import { PiBaseArcChartComponent } from '../base-arc-chart.component';
|
import { PiBaseArcChartComponent } from '../base-arc-chart.component';
|
||||||
|
|
||||||
Chart.register(ArcElement, PieController, Tooltip, Legend);
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pi-pie-chart',
|
selector: 'pi-pie-chart',
|
||||||
templateUrl: './pie-chart.component.html',
|
templateUrl: './pie-chart.component.html',
|
||||||
@@ -12,6 +10,9 @@ Chart.register(ArcElement, PieController, Tooltip, Legend);
|
|||||||
})
|
})
|
||||||
export class PiPieChartComponent extends PiBaseArcChartComponent {
|
export class PiPieChartComponent extends PiBaseArcChartComponent {
|
||||||
protected buildChart(): Chart {
|
protected buildChart(): Chart {
|
||||||
|
// Aquí y no al importar: así una app que nunca pinta este gráfico no
|
||||||
|
// arrastra Chart.js. Registrar dos veces no hace nada.
|
||||||
|
Chart.register(ArcElement, PieController, Tooltip, Legend);
|
||||||
return new Chart(this.chartRef().nativeElement, {
|
return new Chart(this.chartRef().nativeElement, {
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: this.getArcData<'pie'>(),
|
data: this.getArcData<'pie'>(),
|
||||||
|
|||||||
@@ -17,16 +17,6 @@ import { hexToRgba, lineDatasetConfig, tooltipConfig } from '../chart.configs';
|
|||||||
import { PiBaseCartesianChartComponent } from '../base-cartesian-chart.component';
|
import { PiBaseCartesianChartComponent } from '../base-cartesian-chart.component';
|
||||||
import { PiChartLegendComponent } from '../chart-legend/chart-legend.component';
|
import { PiChartLegendComponent } from '../chart-legend/chart-legend.component';
|
||||||
|
|
||||||
Chart.register(
|
|
||||||
RadarController,
|
|
||||||
RadialLinearScale,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
Filler,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A diferencia de `pi-line-chart`/`pi-bar-chart`, la escala radial (`r`) no
|
* A diferencia de `pi-line-chart`/`pi-bar-chart`, la escala radial (`r`) no
|
||||||
* encaja en `scalesConfig` (pensado para `x`/`y`), así que no reutiliza
|
* encaja en `scalesConfig` (pensado para `x`/`y`), así que no reutiliza
|
||||||
@@ -94,6 +84,9 @@ export class PiRadarChartComponent extends PiBaseCartesianChartComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected buildChart(): Chart {
|
protected buildChart(): Chart {
|
||||||
|
// Aquí y no al importar: así una app que nunca pinta este gráfico no
|
||||||
|
// arrastra Chart.js. Registrar dos veces no hace nada.
|
||||||
|
Chart.register(RadarController, RadialLinearScale, LineElement, PointElement, Filler, Tooltip, Legend);
|
||||||
return new Chart(this.chartRef().nativeElement, {
|
return new Chart(this.chartRef().nativeElement, {
|
||||||
type: 'radar',
|
type: 'radar',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ComponentFamilyClass, theming } from '../../../styles';
|
|||||||
import { Date } from '../../../util/models';
|
import { Date } from '../../../util/models';
|
||||||
import { PiBaseChipComponent } from '../base-chip.component';
|
import { PiBaseChipComponent } from '../base-chip.component';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import _ from 'lodash';
|
import { capitalize } from '../../../util/common/compare';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pi-chip-date',
|
selector: 'pi-chip-date',
|
||||||
@@ -73,8 +73,8 @@ export class PiChipDateComponent extends PiBaseChipComponent {
|
|||||||
day: dateObj.day.toString() || '',
|
day: dateObj.day.toString() || '',
|
||||||
month: dateObj.monthShort || '',
|
month: dateObj.monthShort || '',
|
||||||
weekDay: isDateToday
|
weekDay: isDateToday
|
||||||
? _.capitalize(dateObj.toRelativeCalendar() ?? '')
|
? capitalize(dateObj.toRelativeCalendar() ?? '')
|
||||||
: _.capitalize(dateObj.weekdayShort || ''),
|
: capitalize(dateObj.weekdayShort || ''),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export class PiFileUploadComponent implements FormValueControl<File[]> {
|
|||||||
|
|
||||||
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
|
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
|
||||||
readonly disabled = input(false);
|
readonly disabled = input(false);
|
||||||
|
readonly touched = input(false);
|
||||||
readonly required = input(false);
|
readonly required = input(false);
|
||||||
readonly name = input('');
|
readonly name = input('');
|
||||||
|
|
||||||
@@ -94,7 +95,10 @@ export class PiFileUploadComponent implements FormValueControl<File[]> {
|
|||||||
this.errors().map((error) => error.message ?? error.kind),
|
this.errors().map((error) => error.message ?? error.kind),
|
||||||
);
|
);
|
||||||
|
|
||||||
protected readonly hasError = computed(() => this.errors().length > 0);
|
/** El error se ve cuando el campo está tocado (al salir de él o al enviar el formulario), no antes. */
|
||||||
|
protected readonly hasError = computed(
|
||||||
|
() => this.touched() && this.errors().length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
/* --- Arrastrar y soltar ----------------------------------------------- */
|
/* --- Arrastrar y soltar ----------------------------------------------- */
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ export abstract class PiBaseFormFieldComponent<
|
|||||||
this.errors().map((error) => error.message ?? error.kind),
|
this.errors().map((error) => error.message ?? error.kind),
|
||||||
);
|
);
|
||||||
|
|
||||||
protected readonly hasError = computed(() => this.errors().length > 0);
|
/** El error se ve cuando el campo está tocado (al salir de él o al enviar el formulario), no antes. */
|
||||||
|
protected readonly hasError = computed(
|
||||||
|
() => this.touched() && this.errors().length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `MatInput` normalmente deriva `errorState` de un `NgControl` (`ngModel`,
|
* `MatInput` normalmente deriva `errorState` de un `NgControl` (`ngModel`,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export class PiCheckboxComponent implements FormCheckboxControl {
|
|||||||
|
|
||||||
readonly checked = model(false);
|
readonly checked = model(false);
|
||||||
readonly disabled = input(false);
|
readonly disabled = input(false);
|
||||||
|
readonly touched = input(false);
|
||||||
readonly required = input(false);
|
readonly required = input(false);
|
||||||
readonly name = input('');
|
readonly name = input('');
|
||||||
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
|
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
|
||||||
@@ -70,7 +71,10 @@ export class PiCheckboxComponent implements FormCheckboxControl {
|
|||||||
this.errors().map((error) => error.message ?? error.kind),
|
this.errors().map((error) => error.message ?? error.kind),
|
||||||
);
|
);
|
||||||
|
|
||||||
protected readonly hasError = computed(() => this.errors().length > 0);
|
/** El error se ve cuando el campo está tocado (al salir de él o al enviar el formulario), no antes. */
|
||||||
|
protected readonly hasError = computed(
|
||||||
|
() => this.touched() && this.errors().length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `name` solo lo rellena la directiva `FormField`; suelto llega vacío. Sin
|
* `name` solo lo rellena la directiva `FormField`; suelto llega vacío. Sin
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
<mat-form-field [appearance]="appearance()" floatLabel="always">
|
<mat-form-field [appearance]="appearance()" floatLabel="always">
|
||||||
<mat-label><ng-content select="pi-label"></ng-content></mat-label>
|
<mat-label><ng-content select="pi-label"></ng-content></mat-label>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Only when something is actually projected. An empty `matPrefix` container
|
||||||
|
still makes Material treat the field as having an icon prefix, which
|
||||||
|
reserves space and pulls the text 12px left of every other field.
|
||||||
|
-->
|
||||||
|
@if (hasPrefix()) {
|
||||||
<ng-container matPrefix>
|
<ng-container matPrefix>
|
||||||
<ng-content select="pi-prefix"></ng-content>
|
<ng-content select="pi-prefix"></ng-content>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
}
|
||||||
|
|
||||||
<ng-container matSuffix>
|
<ng-container matSuffix>
|
||||||
<ng-content select="pi-suffix"></ng-content>
|
<ng-content select="pi-suffix"></ng-content>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
contentChild,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
import { MatInput } from '@angular/material/input';
|
import { MatInput } from '@angular/material/input';
|
||||||
import { PiAutofocusDirective } from '../directives/autofocus.directive';
|
import { PiAutofocusDirective } from '../directives/autofocus.directive';
|
||||||
import { numericValue } from '../numeric-value';
|
import { numericValue } from '../numeric-value';
|
||||||
|
import { PiPrefixDirective } from '../../../util/affix/affix.directive';
|
||||||
|
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -40,4 +42,7 @@ export class PiNumberFieldComponent extends PiBaseFormFieldComponent<
|
|||||||
@HostBinding('class') className = `pi-form-field__number`;
|
@HostBinding('class') className = `pi-form-field__number`;
|
||||||
|
|
||||||
protected readonly rawValue = numericValue(this.value);
|
protected readonly rawValue = numericValue(this.value);
|
||||||
|
|
||||||
|
/** Whether a consumer projected a `pi-prefix`; see the template. */
|
||||||
|
protected readonly hasPrefix = contentChild(PiPrefixDirective);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export class PiRadioGroupComponent<
|
|||||||
|
|
||||||
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
|
readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
|
||||||
readonly disabled = input(false);
|
readonly disabled = input(false);
|
||||||
|
readonly touched = input(false);
|
||||||
readonly required = input(false);
|
readonly required = input(false);
|
||||||
readonly name = input('');
|
readonly name = input('');
|
||||||
|
|
||||||
@@ -77,7 +78,10 @@ export class PiRadioGroupComponent<
|
|||||||
this.errors().map((error) => error.message ?? error.kind),
|
this.errors().map((error) => error.message ?? error.kind),
|
||||||
);
|
);
|
||||||
|
|
||||||
protected readonly hasError = computed(() => this.errors().length > 0);
|
/** El error se ve cuando el campo está tocado (al salir de él o al enviar el formulario), no antes. */
|
||||||
|
protected readonly hasError = computed(
|
||||||
|
() => this.touched() && this.errors().length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
/** Agrupa los `<input type="radio">`; debe ser único por grupo. */
|
/** Agrupa los `<input type="radio">`; debe ser único por grupo. */
|
||||||
private readonly uniqueName = `pi-radio-group-${nextUniqueId++}`;
|
private readonly uniqueName = `pi-radio-group-${nextUniqueId++}`;
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ export abstract class PiBaseSelectComponent<
|
|||||||
/** Se emite cuando el usuario interactúa; `FormField` lo usa para marcar `touched`. */
|
/** Se emite cuando el usuario interactúa; `FormField` lo usa para marcar `touched`. */
|
||||||
readonly touch = output<void>();
|
readonly touch = output<void>();
|
||||||
|
|
||||||
|
/** El error se ve cuando el campo está tocado, no antes. */
|
||||||
protected get hasError(): boolean {
|
protected get hasError(): boolean {
|
||||||
return this.errors().length > 0;
|
return this.touched() && this.errors().length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import _ from 'lodash';
|
import { isEqual } from '../../../util/common/compare';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
||||||
import { Device } from '../../../util/device-configs';
|
import { Device } from '../../../util/device-configs';
|
||||||
@@ -97,7 +97,7 @@ export class PiSelectCarouselComponent
|
|||||||
* @returns The selected option.
|
* @returns The selected option.
|
||||||
*/
|
*/
|
||||||
get selectedOption(): any {
|
get selectedOption(): any {
|
||||||
return this.options().find((option) => _.isEqual(this.value(), option));
|
return this.options().find((option) => isEqual(this.value(), option));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
||||||
import _ from 'lodash';
|
import { isMatch } from '../../../util/common/compare';
|
||||||
import { MatCheckboxChange, MatCheckbox } from '@angular/material/checkbox';
|
import { MatCheckboxChange, MatCheckbox } from '@angular/material/checkbox';
|
||||||
import { NgTemplateOutlet } from '@angular/common';
|
import { NgTemplateOutlet } from '@angular/common';
|
||||||
import { PiIconComponent } from '../../icon/icon/icon.component';
|
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||||
@@ -114,7 +114,9 @@ export class PiSelectMultipleComponent
|
|||||||
get selectedOptions(): Array<any> {
|
get selectedOptions(): Array<any> {
|
||||||
const value = this.value();
|
const value = this.value();
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
return this.options().filter((option) => _.some(value, option));
|
return this.options().filter((option) =>
|
||||||
|
value.some((selected: unknown) => isMatch(selected, option)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
||||||
import _ from 'lodash';
|
import { isEqual } from '../../../util/common/compare';
|
||||||
import { NgTemplateOutlet } from '@angular/common';
|
import { NgTemplateOutlet } from '@angular/common';
|
||||||
import { PiIconComponent } from '../../icon/icon/icon.component';
|
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||||
import { PiDisabledDirective } from '../../../util/status/directives/status.directive';
|
import { PiDisabledDirective } from '../../../util/status/directives/status.directive';
|
||||||
@@ -103,7 +103,7 @@ export class PiSelectPeriodComponent
|
|||||||
* @returns The selected option.
|
* @returns The selected option.
|
||||||
*/
|
*/
|
||||||
get selectedOption(): any {
|
get selectedOption(): any {
|
||||||
return this.options().find((option) => _.isEqual(this.value(), option));
|
return this.options().find((option) => isEqual(this.value(), option));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
||||||
import _ from 'lodash';
|
import { isEqual } from '../../../util/common/compare';
|
||||||
import { NgTemplateOutlet } from '@angular/common';
|
import { NgTemplateOutlet } from '@angular/common';
|
||||||
import { PiIconComponent } from '../../icon/icon/icon.component';
|
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||||
import { PiDisabledDirective } from '../../../util/status/directives/status.directive';
|
import { PiDisabledDirective } from '../../../util/status/directives/status.directive';
|
||||||
@@ -116,7 +116,7 @@ export class PiSelectComponent
|
|||||||
* @returns The selected option.
|
* @returns The selected option.
|
||||||
*/
|
*/
|
||||||
get selectedOption(): any {
|
get selectedOption(): any {
|
||||||
return this.options().find((option) => _.isEqual(this.value(), option));
|
return this.options().find((option) => isEqual(this.value(), option));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
39
libs/ui/src/lib/util/common/compare.spec.ts
Normal file
39
libs/ui/src/lib/util/common/compare.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { capitalize, isEqual, isMatch } from './compare';
|
||||||
|
|
||||||
|
describe('compare', () => {
|
||||||
|
it('isEqual compara en profundidad', () => {
|
||||||
|
expect(
|
||||||
|
isEqual({ id: 1, tags: ['a', 'b'] }, { tags: ['a', 'b'], id: 1 }),
|
||||||
|
).toBe(true);
|
||||||
|
expect(isEqual({ id: 1 }, { id: 1, extra: true })).toBe(false);
|
||||||
|
expect(isEqual([1, [2, 3]], [1, [2, 3]])).toBe(true);
|
||||||
|
expect(isEqual([1, 2], [2, 1])).toBe(false);
|
||||||
|
expect(isEqual(new Date(0), new Date(0))).toBe(true);
|
||||||
|
expect(isEqual(NaN, NaN)).toBe(true);
|
||||||
|
expect(isEqual(null, undefined)).toBe(false);
|
||||||
|
expect(isEqual('1', 1)).toBe(false);
|
||||||
|
expect(isEqual({ a: { b: 1 } }, { a: { b: 2 } })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isMatch acepta lo que contiene el criterio, como `_.some(lista, objeto)`', () => {
|
||||||
|
const option = { id: 2, name: 'Dos' };
|
||||||
|
expect(
|
||||||
|
[
|
||||||
|
{ id: 1, name: 'Uno' },
|
||||||
|
{ id: 2, name: 'Dos', extra: 1 },
|
||||||
|
].some((v) => isMatch(v, option)),
|
||||||
|
).toBe(true);
|
||||||
|
expect([{ id: 2 }].some((v) => isMatch(v, option))).toBe(false);
|
||||||
|
expect(isMatch({ a: { b: 1, c: 2 } }, { a: { b: 1 } })).toBe(true);
|
||||||
|
expect(isMatch({ tags: ['a', 'b', 'c'] }, { tags: ['c', 'a'] })).toBe(true);
|
||||||
|
expect(isMatch({ tags: ['a'] }, { tags: ['z'] })).toBe(false);
|
||||||
|
expect(isMatch('x', 'x')).toBe(true);
|
||||||
|
expect(isMatch(null, { id: 1 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('capitalize pone la primera en mayúscula y el resto en minúscula', () => {
|
||||||
|
expect(capitalize('lun')).toBe('Lun');
|
||||||
|
expect(capitalize('HOY')).toBe('Hoy');
|
||||||
|
expect(capitalize('')).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
66
libs/ui/src/lib/util/common/compare.ts
Normal file
66
libs/ui/src/lib/util/common/compare.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Las tres utilidades de lodash que usaba la librería (`isEqual`, `some` con
|
||||||
|
* un objeto como criterio y `capitalize`), con la misma semántica en los
|
||||||
|
* casos que se dan aquí: valores JSON, fechas, arrays y objetos planos. Sin
|
||||||
|
* lodash, `@pi/ui` no arrastra una librería CommonJS al bundle de nadie.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Igualdad profunda: `isEqual({ a: [1] }, { a: [1] })` es `true`. */
|
||||||
|
export function isEqual(a: unknown, b: unknown): boolean {
|
||||||
|
if (Object.is(a, b)) return true;
|
||||||
|
if (a instanceof Date && b instanceof Date)
|
||||||
|
return a.getTime() === b.getTime();
|
||||||
|
if (Array.isArray(a) || Array.isArray(b)) {
|
||||||
|
return (
|
||||||
|
Array.isArray(a) &&
|
||||||
|
Array.isArray(b) &&
|
||||||
|
a.length === b.length &&
|
||||||
|
a.every((item, i) => isEqual(item, b[i]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
||||||
|
const keys = Object.keys(a);
|
||||||
|
return (
|
||||||
|
keys.length === Object.keys(b).length &&
|
||||||
|
keys.every((key) => hasOwn(b, key) && isEqual(a[key], b[key]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Si `value` contiene lo que pide `source`: cada propiedad de `source` está
|
||||||
|
* en `value` con un valor que a su vez la contiene (y el resto da igual).
|
||||||
|
* Es lo que hace `_.some(lista, objeto)` con cada elemento.
|
||||||
|
*/
|
||||||
|
export function isMatch(value: unknown, source: unknown): boolean {
|
||||||
|
if (isPlainObject(source)) {
|
||||||
|
return (
|
||||||
|
isPlainObject(value) &&
|
||||||
|
Object.keys(source).every(
|
||||||
|
(key) => hasOwn(value, key) && isMatch(value[key], source[key]),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Array.isArray(source)) {
|
||||||
|
// Como lodash: cada elemento pedido tiene que estar en el array.
|
||||||
|
return (
|
||||||
|
Array.isArray(value) &&
|
||||||
|
source.every((wanted) => value.some((item) => isMatch(item, wanted)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return isEqual(value, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Primera letra en mayúscula y el resto en minúscula: «lun» → «Lun». */
|
||||||
|
export function capitalize(text: string): string {
|
||||||
|
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
if (typeof value !== 'object' || value === null) return false;
|
||||||
|
const proto = Object.getPrototypeOf(value);
|
||||||
|
return proto === Object.prototype || proto === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOwn(object: object, key: string): boolean {
|
||||||
|
return Object.prototype.hasOwnProperty.call(object, key);
|
||||||
|
}
|
||||||
328
package-lock.json
generated
328
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,6 @@
|
|||||||
"@ngx-translate/http-loader": "^18.0.0",
|
"@ngx-translate/http-loader": "^18.0.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"lodash": "^4.18.1",
|
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"ngx-infinite-scroll": "^22.0.0",
|
"ngx-infinite-scroll": "^22.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
@@ -71,7 +70,6 @@
|
|||||||
"@swc/helpers": "0.5.18",
|
"@swc/helpers": "0.5.18",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jest": "~30.0.0",
|
"@types/jest": "~30.0.0",
|
||||||
"@types/lodash": "^4.17.24",
|
|
||||||
"@types/luxon": "^3.7.2",
|
"@types/luxon": "^3.7.2",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user