mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 14:32:10 +01:00
Merged PR 1866: Anlage Komponenten und Directives + Unit Tests und Stories
Related work items: #5175
This commit is contained in:
committed by
Nino Righi
parent
b21ebac53f
commit
4cf0ce820e
@@ -2,8 +2,11 @@ import type { StorybookConfig } from '@storybook/angular';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../stories/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
staticDirs: ['../src/assets'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-docs',
|
||||
],
|
||||
previewHead: (head) => `
|
||||
${head}
|
||||
<link href="/assets/fonts/fonts.css" rel="stylesheet" />
|
||||
|
||||
@@ -118,7 +118,19 @@
|
||||
"configDir": "apps/isa-app/.storybook",
|
||||
"browserTarget": "isa-app:build",
|
||||
"compodoc": false,
|
||||
"styles": ["apps/isa-app/src/ui.scss", "apps/isa-app/src/styles.scss"]
|
||||
"open": false,
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/isa-app/src/assets",
|
||||
"output": "/assets"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/ui.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
@@ -133,33 +145,18 @@
|
||||
"outputDir": "dist/storybook/isa-app",
|
||||
"configDir": "apps/isa-app/.storybook",
|
||||
"browserTarget": "isa-app:build",
|
||||
"compodoc": false
|
||||
"compodoc": false,
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/ui.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"quiet": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"test-storybook": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "test-storybook -c apps/isa-app/.storybook --url=http://localhost:4400"
|
||||
}
|
||||
},
|
||||
"static-storybook": {
|
||||
"executor": "@nx/web:file-server",
|
||||
"dependsOn": ["build-storybook"],
|
||||
"options": {
|
||||
"buildTarget": "isa-app:build-storybook",
|
||||
"staticFilePath": "dist/storybook/isa-app",
|
||||
"spa": true
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"buildTarget": "isa-app:build-storybook:ci"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@use '../../../libs/ui/buttons/src/buttons.scss';
|
||||
@use '../../../libs/ui/datepicker/src/datepicker.scss';
|
||||
@use '../../../libs/ui/dialog/src/dialog.scss';
|
||||
@use '../../../libs/ui/input-controls/src/input-controls.scss';
|
||||
@use '../../../libs/ui/menu/src/menu.scss';
|
||||
@use '../../../libs/ui/progress-bar/src/lib/progress-bar.scss';
|
||||
@use '../../../libs/ui/tooltip/src/tooltip.scss';
|
||||
@use "../../../libs/ui/buttons/src/buttons.scss";
|
||||
@use "../../../libs/ui/datepicker/src/datepicker.scss";
|
||||
@use "../../../libs/ui/dialog/src/dialog.scss";
|
||||
@use "../../../libs/ui/input-controls/src/input-controls.scss";
|
||||
@use "../../../libs/ui/menu/src/menu.scss";
|
||||
@use "../../../libs/ui/progress-bar/src/lib/progress-bar.scss";
|
||||
@use "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
|
||||
@use "../../../libs/ui/tooltip/src/tooltip.scss";
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
applicationConfig,
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import {
|
||||
ProductInfoComponent,
|
||||
ProductInfoItem,
|
||||
ProductInfoOrientation,
|
||||
} from '@isa/remission/shared/product';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
type ProductInfoInputs = {
|
||||
item: ProductInfoItem;
|
||||
orientation: ProductInfoOrientation;
|
||||
};
|
||||
|
||||
const meta: Meta<ProductInfoInputs> = {
|
||||
component: ProductInfoComponent,
|
||||
title: 'remission/shared/product/ProductInfoComponent',
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [
|
||||
provideRouter([{ path: ':ean', component: ProductInfoComponent }]),
|
||||
],
|
||||
}),
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
provideProductRouterLinkBuilder((ean: string) => ean),
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
item: {
|
||||
product: {
|
||||
contributors: 'John Doe',
|
||||
ean: '9783742327529',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
name: 'Test Product',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
currency: 'EUR',
|
||||
value: 19.99,
|
||||
},
|
||||
},
|
||||
},
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
argTypes: {
|
||||
item: {
|
||||
control: 'object',
|
||||
description: 'The product item to display.',
|
||||
},
|
||||
orientation: {
|
||||
control: 'select',
|
||||
options: ['horizontal', 'vertical'],
|
||||
description: 'The orientation of the product info display.',
|
||||
table: {
|
||||
defaultValue: {
|
||||
summary: 'horizontal',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<remi-product-info ${argsToTemplate(args)}></remi-product-info>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductInfoInputs>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
item: {
|
||||
product: {
|
||||
contributors: 'John Doe',
|
||||
ean: '9783742327529',
|
||||
format: 'HC',
|
||||
formatDetail: 'Hardcover (gebunden)',
|
||||
name: 'Sample Product',
|
||||
},
|
||||
retailPrice: {
|
||||
value: {
|
||||
currency: 'EUR',
|
||||
value: 29.99,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { ProductStockInfoComponent } from '@isa/remission/shared/product';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
|
||||
const meta: Meta<ProductStockInfoComponent> = {
|
||||
component: ProductStockInfoComponent,
|
||||
title: 'remission/shared/product/ProductStockInfoComponent',
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
provideProductRouterLinkBuilder((ean: string) => ean),
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
stock: 100,
|
||||
stockToRemit: 20,
|
||||
},
|
||||
argTypes: {
|
||||
stock: {
|
||||
control: { type: 'number' },
|
||||
description: 'The current stock of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
stockToRemit: {
|
||||
control: { type: 'number' },
|
||||
description: 'The amount of stock to remit.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<remi-product-stock-info ${argsToTemplate({ info: args })}></remi-product-stock-info>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductStockInfoComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Meta, argsToTemplate } from '@storybook/angular';
|
||||
import { ProductFormatIconGroup } from '@isa/icons';
|
||||
import { ProductFormatIconComponent } from '@isa/shared/product-foramt';
|
||||
|
||||
type ProductFormatInputs = {
|
||||
format: string;
|
||||
};
|
||||
|
||||
const options = Object.keys(ProductFormatIconGroup).map((key) =>
|
||||
key.toUpperCase(),
|
||||
);
|
||||
|
||||
const meta: Meta<ProductFormatInputs> = {
|
||||
title: 'shared/product-format/ProductFormatIcon',
|
||||
component: ProductFormatIconComponent,
|
||||
argTypes: {
|
||||
format: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options,
|
||||
description: 'The product format to display the icon for.',
|
||||
defaultValue: options[0],
|
||||
},
|
||||
},
|
||||
args: {
|
||||
format: options[0], // Default value for the product format
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<shared-product-format-icon ${argsToTemplate(args)}></shared-product-format-icon>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = typeof meta;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { argsToTemplate, Meta } from '@storybook/angular';
|
||||
import { ProductFormatIconGroup } from '@isa/icons';
|
||||
import { ProductFormatComponent } from '@isa/shared/product-foramt';
|
||||
|
||||
type ProductFormatInputs = {
|
||||
format: string;
|
||||
formatDetail: string;
|
||||
};
|
||||
|
||||
const options = Object.keys(ProductFormatIconGroup).map((key) =>
|
||||
key.toUpperCase(),
|
||||
);
|
||||
|
||||
const meta: Meta<ProductFormatInputs> = {
|
||||
title: 'shared/product-format/ProductFormat',
|
||||
component: ProductFormatComponent,
|
||||
argTypes: {
|
||||
format: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options,
|
||||
description: 'The product format to display the icon for.',
|
||||
defaultValue: options[0],
|
||||
},
|
||||
formatDetail: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
description: 'The detail text for the product format.',
|
||||
defaultValue: 'Default Format Detail',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
format: options[0], // Default value for the product format
|
||||
formatDetail: 'Default Format Detail', // Default value for the format detail
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<shared-product-format ${argsToTemplate(args)}></shared-product-format>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = typeof meta;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -1,5 +1,13 @@
|
||||
import { argsToTemplate, type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';
|
||||
import { ProductImageDirective, provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import {
|
||||
argsToTemplate,
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import {
|
||||
ProductImageDirective,
|
||||
provideProductImageUrl,
|
||||
} from '@isa/shared/product-image';
|
||||
|
||||
type ProductImageInputs = {
|
||||
ean: string;
|
||||
@@ -13,7 +21,9 @@ const meta: Meta<ProductImageInputs> = {
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [ProductImageDirective],
|
||||
providers: [provideProductImageUrl('https://produktbilder-test.paragon-data.net')],
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';
|
||||
import { ClientRowComponent, ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
|
||||
import {
|
||||
ClientRowComponent,
|
||||
ClientRowImports,
|
||||
ItemRowDataImports,
|
||||
} from '@isa/ui/item-rows';
|
||||
|
||||
const meta: Meta<ClientRowComponent> = {
|
||||
component: ClientRowComponent,
|
||||
@@ -21,7 +25,7 @@ const meta: Meta<ClientRowComponent> = {
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label>Belegdatum</ui-item-row-data-label>
|
||||
<ui-item-row-data-value>
|
||||
<span class="isa-text-body-2-bold">
|
||||
<span class="isa-text-body-2-bold">
|
||||
01.11.2024
|
||||
</span>
|
||||
</ui-item-row-data-value>
|
||||
@@ -29,17 +33,17 @@ const meta: Meta<ClientRowComponent> = {
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label>Rechnugsnr.</ui-item-row-data-label>
|
||||
<ui-item-row-data-value>
|
||||
<span class="isa-text-body-2-bold">
|
||||
<span class="isa-text-body-2-bold">
|
||||
1234567890
|
||||
</span>
|
||||
</ui-item-row-data-value>
|
||||
</ui-item-row-data-row>
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label >Vorgangs-ID</ui-item-row-data-label>
|
||||
<ui-item-row-data-value >
|
||||
<span class="isa-text-body-2-bold">
|
||||
640175214390060/0
|
||||
</span>
|
||||
<ui-item-row-data-value >
|
||||
<span class="isa-text-body-2-bold">
|
||||
640175214390060/0
|
||||
</span>
|
||||
</ui-item-row-data-value>
|
||||
</ui-item-row-data-row>
|
||||
</ui-item-row-data>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/angular';
|
||||
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
|
||||
|
||||
type SkeletonLoaderDirectiveInputs = {
|
||||
uiSkeletonLoader: boolean;
|
||||
uiSkeletonLoaderWidth?: string;
|
||||
uiSkeletonLoaderHeight?: string;
|
||||
};
|
||||
|
||||
const meta: Meta<SkeletonLoaderDirectiveInputs> = {
|
||||
component: SkeletonLoaderDirective,
|
||||
title: 'ui/skeleton-loader/SkeletonLoaderDirective',
|
||||
args: {
|
||||
uiSkeletonLoader: false,
|
||||
uiSkeletonLoaderWidth: '100px',
|
||||
uiSkeletonLoaderHeight: '',
|
||||
},
|
||||
argTypes: {
|
||||
uiSkeletonLoader: { control: 'boolean' },
|
||||
uiSkeletonLoaderWidth: { control: 'text' },
|
||||
uiSkeletonLoaderHeight: { control: 'text' },
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<div *uiSkeletonLoader="${args.uiSkeletonLoader}; width: '${args.uiSkeletonLoaderWidth}'; height: '${args.uiSkeletonLoaderHeight}'"> This is my content! </div>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<SkeletonLoaderDirectiveInputs>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/angular';
|
||||
import { SkeletonLoaderComponent } from '@isa/ui/skeleton-loader';
|
||||
|
||||
const meta: Meta<SkeletonLoaderComponent> = {
|
||||
component: SkeletonLoaderComponent,
|
||||
title: 'ui/skeleton-loader/SkeletonLoader',
|
||||
render: () => ({
|
||||
template: `<ui-skeleton-loader class="w-24"></ui-skeleton-loader>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<SkeletonLoaderComponent>;
|
||||
|
||||
export const Default: Story = {};
|
||||
54
eslint.config.js
Normal file
54
eslint.config.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const eslintConfigPrettier = require('eslint-config-prettier/flat');
|
||||
|
||||
module.exports = [
|
||||
...nx.configs['flat/base'],
|
||||
...nx.configs['flat/typescript'],
|
||||
...nx.configs['flat/javascript'],
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
ignores: ['**/dist'],
|
||||
},
|
||||
// {
|
||||
// files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
// rules: {
|
||||
// '@nx/enforce-module-boundaries': [
|
||||
// 'error',
|
||||
// {
|
||||
// enforceBuildableLibDependency: true,
|
||||
// allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'],
|
||||
// depConstraints: [
|
||||
// {
|
||||
// sourceTag: '*',
|
||||
// onlyDependOnLibsWithTags: ['*'],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
// Override or add rules here
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
plugins: {
|
||||
prettier: require('eslint-plugin-prettier'),
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
rules: {
|
||||
'@nx/dependency-checks': [
|
||||
'error',
|
||||
{
|
||||
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
|
||||
},
|
||||
],
|
||||
},
|
||||
languageOptions: {
|
||||
parser: require('jsonc-eslint-parser'),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export * from './lib/models';
|
||||
|
||||
4
libs/remission/data-access/src/lib/models/index.ts
Normal file
4
libs/remission/data-access/src/lib/models/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './price-value';
|
||||
export * from './price';
|
||||
export * from './product';
|
||||
export * from './return-item';
|
||||
6
libs/remission/data-access/src/lib/models/price-value.ts
Normal file
6
libs/remission/data-access/src/lib/models/price-value.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { PriceValueDTO } from '@generated/swagger/inventory-api';
|
||||
|
||||
export interface PriceValue extends PriceValueDTO {
|
||||
value: number;
|
||||
currency: string;
|
||||
}
|
||||
6
libs/remission/data-access/src/lib/models/price.ts
Normal file
6
libs/remission/data-access/src/lib/models/price.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { PriceDTO } from '@generated/swagger/inventory-api';
|
||||
import { PriceValue } from './price-value';
|
||||
|
||||
export interface Price extends PriceDTO {
|
||||
value: PriceValue;
|
||||
}
|
||||
9
libs/remission/data-access/src/lib/models/product.ts
Normal file
9
libs/remission/data-access/src/lib/models/product.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ProductDTO } from '@generated/swagger/inventory-api';
|
||||
|
||||
export interface Product extends ProductDTO {
|
||||
name: string;
|
||||
contributors: string;
|
||||
ean: string;
|
||||
format: string;
|
||||
formatDetail: string;
|
||||
}
|
||||
8
libs/remission/data-access/src/lib/models/return-item.ts
Normal file
8
libs/remission/data-access/src/lib/models/return-item.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ReturnItemDTO } from '@generated/swagger/inventory-api';
|
||||
import { Product } from './product';
|
||||
import { Price } from './price';
|
||||
|
||||
export interface ReturnItem extends ReturnItemDTO {
|
||||
product: Product;
|
||||
retailPrice: Price;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const greeting = 'Hello World!';
|
||||
2
libs/remission/shared/product/src/index.ts
Normal file
2
libs/remission/shared/product/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lib/product-info/product-info.component';
|
||||
export * from './lib/product-stock-info/product-stock-info.component';
|
||||
@@ -0,0 +1,46 @@
|
||||
@let product = item().product;
|
||||
@let price = item().retailPrice;
|
||||
@let horizontal = orientation() === 'horizontal';
|
||||
<div>
|
||||
<img
|
||||
class="w-full h-auto object-contain"
|
||||
sharedProductRouterLink
|
||||
sharedProductImage
|
||||
[ean]="product.ean"
|
||||
[alt]="product.name"
|
||||
data-what="product-image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid"
|
||||
[class.grid-cols-[minmax(20rem,1fr),auto]]="horizontal"
|
||||
[class.gap-6]="horizontal"
|
||||
[class.grid-flow-row]="!horizontal"
|
||||
[class.gap-2]="!horizontal"
|
||||
>
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
<div class="isa-text-body-2-bold" data-what="product-contributors">
|
||||
{{ product.contributors }}
|
||||
</div>
|
||||
<div class="isa-text-subtitle-1-regular" data-what="product-name">
|
||||
{{ product.name }}
|
||||
</div>
|
||||
<div class="isa-text-body-2-bold" data-what="product-price">
|
||||
{{ price.value.value | currency: price.value.currencySymbol }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-2 items-start justify-end">
|
||||
<shared-product-format
|
||||
[format]="product.format"
|
||||
[formatDetail]="product.formatDetail"
|
||||
data-what="product-format"
|
||||
></shared-product-format>
|
||||
<div
|
||||
class="isa-text-body-2-regular text-isa-neutral-600"
|
||||
data-what="product-ean"
|
||||
>
|
||||
{{ product.ean }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import {
|
||||
ProductInfoComponent,
|
||||
ProductInfoItem,
|
||||
} from './product-info.component';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
|
||||
import { MockDirectives } from 'ng-mocks';
|
||||
|
||||
describe('ProductInfoComponent', () => {
|
||||
let spectator: Spectator<ProductInfoComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: ProductInfoComponent,
|
||||
overrideComponents: [
|
||||
[
|
||||
ProductInfoComponent,
|
||||
{
|
||||
remove: {
|
||||
imports: [ProductImageDirective, ProductRouterLinkDirective],
|
||||
},
|
||||
add: {
|
||||
imports: MockDirectives(
|
||||
ProductImageDirective,
|
||||
ProductRouterLinkDirective,
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const mockItem: ProductInfoItem = {
|
||||
product: {
|
||||
ean: '1234567890123',
|
||||
name: 'Test Product',
|
||||
contributors: '',
|
||||
format: '',
|
||||
formatDetail: '',
|
||||
},
|
||||
retailPrice: {
|
||||
value: { value: 19.99, currency: 'EUR' },
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({ props: { item: mockItem } });
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set data-what and data-which attributes', () => {
|
||||
const host = spectator.element;
|
||||
expect(host).toHaveAttribute('data-what', 'product-info');
|
||||
expect(host).toHaveAttribute('data-which', 'remission-product');
|
||||
});
|
||||
|
||||
it('should set data-ean attribute from item.product.ean', () => {
|
||||
const host = spectator.element;
|
||||
expect(host).toHaveAttribute('data-ean', mockItem.product.ean);
|
||||
});
|
||||
|
||||
// Add more tests for template rendering if template is available
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { ReturnItem } from '@isa/remission/data-access';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
|
||||
import { ProductFormatComponent } from '@isa/shared/product-foramt';
|
||||
|
||||
export type ProductInfoItem = Pick<ReturnItem, 'product' | 'retailPrice'>;
|
||||
|
||||
export type ProductInfoOrientation = 'horizontal' | 'vertical';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-product-info',
|
||||
templateUrl: 'product-info.component.html',
|
||||
imports: [
|
||||
ProductImageDirective,
|
||||
ProductRouterLinkDirective,
|
||||
CurrencyPipe,
|
||||
ProductFormatComponent,
|
||||
],
|
||||
host: {
|
||||
'[class]': 'classList',
|
||||
'data-what': 'product-info',
|
||||
'data-which': 'remission-product',
|
||||
'[attr.data-ean]': 'item().product.ean',
|
||||
},
|
||||
})
|
||||
export class ProductInfoComponent {
|
||||
readonly classList: ReadonlyArray<string> = [
|
||||
'grid',
|
||||
'grid-cols-[3.5rem,1fr]',
|
||||
'gap-6',
|
||||
'text-isa-neutral-900',
|
||||
];
|
||||
|
||||
item = input.required<ProductInfoItem>();
|
||||
|
||||
orientation = input<ProductInfoOrientation>('horizontal');
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<div
|
||||
class="product-stock-info-row"
|
||||
data-what="stock-info-row"
|
||||
data-which="current-stock"
|
||||
>
|
||||
<div data-what="stock-label" data-which="current-stock">
|
||||
Aktueller Bestand
|
||||
</div>
|
||||
<div
|
||||
class="isa-text-body-2-bold"
|
||||
data-what="stock-value"
|
||||
data-which="current-stock"
|
||||
>
|
||||
{{ stock() }}x
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="product-stock-info-row"
|
||||
data-what="stock-info-row"
|
||||
data-which="remit-amount"
|
||||
>
|
||||
<div data-what="stock-label" data-which="remit-amount">Remi Menge</div>
|
||||
<div
|
||||
class="isa-text-body-2-bold"
|
||||
data-what="stock-value"
|
||||
data-which="remit-amount"
|
||||
>
|
||||
{{ stockToRemit() }}x
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="product-stock-info-row"
|
||||
data-what="stock-info-row"
|
||||
data-which="remaining-stock"
|
||||
>
|
||||
<div data-what="stock-label" data-which="remaining-stock">
|
||||
Übriger Bestand
|
||||
</div>
|
||||
<div
|
||||
class="isa-text-body-2-bold"
|
||||
data-what="stock-value"
|
||||
data-which="remaining-stock"
|
||||
>
|
||||
{{ targetStock() }}x
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-stock-info-row" data-what="stock-info-row" data-which="zob">
|
||||
<div data-what="stock-label" data-which="zob">ZOB</div>
|
||||
<div
|
||||
class="isa-text-body-2-bold grid-flow-row"
|
||||
data-what="stock-value"
|
||||
data-which="zob"
|
||||
>
|
||||
{{ zob() }}x
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
:host {
|
||||
@apply grid grid-flow-row gap-2 text-isa-neutral-900;
|
||||
}
|
||||
|
||||
.product-stock-info-row {
|
||||
@apply flex items-center gap-2 justify-between;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ProductStockInfoComponent } from './product-stock-info.component';
|
||||
|
||||
describe('ProductStockInfoComponent', () => {
|
||||
let spectator: Spectator<ProductStockInfoComponent>;
|
||||
const createComponent = createComponentFactory(ProductStockInfoComponent);
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display the current stock', () => {
|
||||
spectator.setInput('stock', 42);
|
||||
spectator.detectChanges();
|
||||
const value = spectator.query(
|
||||
'[data-what="stock-value"][data-which="current-stock"]',
|
||||
);
|
||||
expect(value).toHaveText('42x');
|
||||
});
|
||||
|
||||
it('should display the remit amount', () => {
|
||||
spectator.setInput('stockToRemit', 7);
|
||||
spectator.detectChanges();
|
||||
const value = spectator.query(
|
||||
'[data-what="stock-value"][data-which="remit-amount"]',
|
||||
);
|
||||
expect(value).toHaveText('7x');
|
||||
});
|
||||
|
||||
it('should display the remaining stock (targetStock)', () => {
|
||||
spectator.setInput('stock', 20);
|
||||
spectator.setInput('stockToRemit', 5);
|
||||
spectator.detectChanges();
|
||||
const value = spectator.query(
|
||||
'[data-what="stock-value"][data-which="remaining-stock"]',
|
||||
);
|
||||
expect(value).toHaveText('15x');
|
||||
});
|
||||
|
||||
it('should display the zob value', () => {
|
||||
spectator.component.zob.set(99);
|
||||
spectator.detectChanges();
|
||||
const value = spectator.query(
|
||||
'[data-what="stock-value"][data-which="zob"]',
|
||||
);
|
||||
expect(value).toHaveText('99x');
|
||||
});
|
||||
|
||||
it('should render all labels with correct e2e attributes', () => {
|
||||
const labels = [
|
||||
{ which: 'current-stock', text: 'Aktueller Bestand' },
|
||||
{ which: 'remit-amount', text: 'Remi Menge' },
|
||||
{ which: 'remaining-stock', text: 'Übriger Bestand' },
|
||||
{ which: 'zob', text: 'ZOB' },
|
||||
];
|
||||
labels.forEach(({ which, text }) => {
|
||||
const label = spectator.query(
|
||||
`[data-what="stock-label"][data-which="${which}"]`,
|
||||
);
|
||||
expect(label).toHaveText(text);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'remi-product-stock-info',
|
||||
templateUrl: './product-stock-info.component.html',
|
||||
styleUrls: ['./product-stock-info.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ProductStockInfoComponent {
|
||||
stock = input<number>(0);
|
||||
|
||||
stockToRemit = input<number>(0);
|
||||
|
||||
targetStock = computed(() => {
|
||||
const stock = this.stock();
|
||||
const stockToRemit = this.stockToRemit();
|
||||
return stock - stockToRemit;
|
||||
});
|
||||
|
||||
zob = signal(0);
|
||||
}
|
||||
7
libs/shared/product-format/README.md
Normal file
7
libs/shared/product-format/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# shared-product-format
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test shared-product-format` to execute the unit tests.
|
||||
34
libs/shared/product-format/eslint.config.js
Normal file
34
libs/shared/product-format/eslint.config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'shared',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'shared',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
libs/shared/product-format/jest.config.ts
Normal file
21
libs/shared/product-format/jest.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'shared-product-format',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/libs/shared/product-format',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
20
libs/shared/product-format/project.json
Normal file
20
libs/shared/product-format/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "shared-product-format",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/product-format/src",
|
||||
"prefix": "shared",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/product-format/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
libs/shared/product-format/src/index.ts
Normal file
2
libs/shared/product-format/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lib/product-format-icon.component';
|
||||
export * from './lib/product-format.component';
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ProductFormatIconComponent } from './product-format-icon.component';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
|
||||
describe('ProductFormatIconComponent', () => {
|
||||
let spectator: Spectator<ProductFormatIconComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: ProductFormatIconComponent,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({ props: { format: 'HC' } });
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render ng-icon with lowercased format as name', () => {
|
||||
const icon = spectator.query(NgIcon);
|
||||
expect(icon).toBeTruthy();
|
||||
expect(icon?.name()).toBe('hc');
|
||||
});
|
||||
|
||||
it('should set correct data attributes on host', () => {
|
||||
const host = spectator.element;
|
||||
expect(host).toHaveAttribute('data-what', 'product-format-icon');
|
||||
expect(host).toHaveAttribute('data-which', 'shared-product-format-icon');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { ProductFormatIconGroup } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-product-format-icon',
|
||||
template: `<ng-icon [name]="formatLowerCase()" size="1.5rem"></ng-icon>`,
|
||||
imports: [NgIcon],
|
||||
providers: [provideIcons(ProductFormatIconGroup)],
|
||||
host: {
|
||||
'[class]': '["inline-block","text-isa-neutral-900", "size-6"]',
|
||||
'data-what': 'product-format-icon',
|
||||
'data-which': 'shared-product-format-icon',
|
||||
},
|
||||
})
|
||||
export class ProductFormatIconComponent {
|
||||
format = input.required<string>();
|
||||
|
||||
formatLowerCase = computed(() => this.format().toLowerCase());
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ProductFormatComponent } from './product-format.component';
|
||||
import { ProductFormatIconComponent } from './product-format-icon.component';
|
||||
|
||||
describe('ProductFormatComponent', () => {
|
||||
let spectator: Spectator<ProductFormatComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: ProductFormatComponent,
|
||||
imports: [ProductFormatIconComponent],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({
|
||||
props: { format: 'HC', formatDetail: 'Hardcover' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render ProductFormatIconComponent with correct format', () => {
|
||||
const icon = spectator.query(ProductFormatIconComponent);
|
||||
expect(icon).toBeTruthy();
|
||||
// Check input binding
|
||||
expect(icon?.format()).toBe('HC');
|
||||
});
|
||||
|
||||
it('should render the formatDetail text', () => {
|
||||
expect(spectator.query('span')).toHaveText('Hardcover');
|
||||
});
|
||||
|
||||
it('should set correct data attributes on host', () => {
|
||||
const host = spectator.element;
|
||||
expect(host).toHaveAttribute('data-what', 'product-format');
|
||||
expect(host).toHaveAttribute('data-which', 'shared-product-format');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { ProductFormatIconComponent } from './product-format-icon.component';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-product-format',
|
||||
template: `<shared-product-format-icon
|
||||
[format]="format()"
|
||||
></shared-product-format-icon>
|
||||
<span class="isa-text-body-2-regular text-isa-secondary-900 truncate">
|
||||
{{ formatDetail() }}
|
||||
</span>`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ProductFormatIconComponent],
|
||||
host: {
|
||||
'[class]': '["flex", "items-center", "gap-2"]',
|
||||
'data-what': 'product-format',
|
||||
'data-which': 'shared-product-format',
|
||||
},
|
||||
})
|
||||
export class ProductFormatComponent {
|
||||
format = input.required<string>();
|
||||
|
||||
formatDetail = input.required<string>();
|
||||
}
|
||||
10
libs/shared/product-format/src/test-setup.ts
Normal file
10
libs/shared/product-format/src/test-setup.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
|
||||
globalThis.ngJest = {
|
||||
testEnvironmentOptions: {
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
},
|
||||
};
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv();
|
||||
28
libs/shared/product-format/tsconfig.json
Normal file
28
libs/shared/product-format/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
17
libs/shared/product-format/tsconfig.lib.json
Normal file
17
libs/shared/product-format/tsconfig.lib.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
16
libs/shared/product-format/tsconfig.spec.json
Normal file
16
libs/shared/product-format/tsconfig.spec.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
7
libs/ui/skeleton-loader/README.md
Normal file
7
libs/ui/skeleton-loader/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# ui-skeleton-loader
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test ui-skeleton-loader` to execute the unit tests.
|
||||
34
libs/ui/skeleton-loader/eslint.config.js
Normal file
34
libs/ui/skeleton-loader/eslint.config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'ui',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'ui',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
libs/ui/skeleton-loader/jest.config.ts
Normal file
21
libs/ui/skeleton-loader/jest.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'ui-skeleton-loader',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/libs/ui/skeleton-loader',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
20
libs/ui/skeleton-loader/project.json
Normal file
20
libs/ui/skeleton-loader/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "ui-skeleton-loader",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/ui/skeleton-loader/src",
|
||||
"prefix": "ui",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/ui/skeleton-loader/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
libs/ui/skeleton-loader/src/index.ts
Normal file
2
libs/ui/skeleton-loader/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lib/skeleton-loader.directive';
|
||||
export * from './lib/skeleton-loader.component';
|
||||
@@ -0,0 +1 @@
|
||||
<div class="ui-skeleton-loader-bar"></div>
|
||||
11
libs/ui/skeleton-loader/src/lib/skeleton-loader.component.ts
Normal file
11
libs/ui/skeleton-loader/src/lib/skeleton-loader.component.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-skeleton-loader',
|
||||
templateUrl: 'skeleton-loader.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
class: 'ui-skeleton-loader',
|
||||
},
|
||||
})
|
||||
export class SkeletonLoaderComponent {}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
import { Spectator, createComponentFactory } from '@ngneat/spectator/jest';
|
||||
import { SkeletonLoaderDirective } from './skeleton-loader.directive';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div
|
||||
*uiSkeletonLoader="isLoading(); width: width(); height: height()"
|
||||
data-testid="content"
|
||||
>
|
||||
Original Content
|
||||
</div>
|
||||
`,
|
||||
imports: [SkeletonLoaderDirective],
|
||||
})
|
||||
class TestHostComponent {
|
||||
isLoading = input(false);
|
||||
width = input<string | undefined>(undefined);
|
||||
height = input<string | undefined>(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unit tests for the SkeletonLoaderDirective.
|
||||
* Tests conditional rendering of skeleton loader vs original content
|
||||
* and verifies custom dimensions are applied correctly.
|
||||
*/
|
||||
describe('SkeletonLoaderDirective', () => {
|
||||
let spectator: Spectator<TestHostComponent>;
|
||||
const createTestHost = createComponentFactory(TestHostComponent);
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createTestHost();
|
||||
spectator.detectChanges();
|
||||
});
|
||||
|
||||
it('should render the Original Content when isLoading is false', () => {
|
||||
expect(spectator.query('[data-testid="content"]')).toHaveText(
|
||||
'Original Content',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the skeleton loader when isLoading is true', () => {
|
||||
spectator.setInput('isLoading', true);
|
||||
expect(spectator.query('ui-skeleton-loader')).toExist();
|
||||
expect(spectator.query('[data-testid="content"]')).not.toExist();
|
||||
});
|
||||
});
|
||||
81
libs/ui/skeleton-loader/src/lib/skeleton-loader.directive.ts
Normal file
81
libs/ui/skeleton-loader/src/lib/skeleton-loader.directive.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
Directive,
|
||||
ViewContainerRef,
|
||||
TemplateRef,
|
||||
inject,
|
||||
input,
|
||||
effect,
|
||||
ComponentRef,
|
||||
untracked,
|
||||
EmbeddedViewRef,
|
||||
} from '@angular/core';
|
||||
import { SkeletonLoaderComponent } from './skeleton-loader.component';
|
||||
|
||||
/**
|
||||
* Structural directive that conditionally replaces its content with a skeleton loader.
|
||||
*
|
||||
* @example
|
||||
* <div *uiSkeletonLoader="isLoading">Content to hide while loading</div>
|
||||
*
|
||||
* @example
|
||||
* <div *uiSkeletonLoader="isLoading; width: '100px'; height: '20px'">Content</div>
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[uiSkeletonLoader]',
|
||||
})
|
||||
export class SkeletonLoaderDirective {
|
||||
#viewContainerRef = inject(ViewContainerRef);
|
||||
#templateRef = inject(TemplateRef);
|
||||
|
||||
private componentRef: ComponentRef<SkeletonLoaderComponent> | null = null;
|
||||
private embeddedViewRef: EmbeddedViewRef<unknown> | null = null;
|
||||
|
||||
uiSkeletonLoader = input<boolean>(false);
|
||||
|
||||
uiSkeletonLoaderWidth = input<string | undefined>(undefined);
|
||||
|
||||
uiSkeletonLoaderHeight = input<string | undefined>(undefined);
|
||||
|
||||
render = effect(() => {
|
||||
const condition = this.uiSkeletonLoader();
|
||||
|
||||
if (condition && !this.componentRef) {
|
||||
// Create the skeleton loader when condition is true and view isn't created
|
||||
this.#viewContainerRef.clear();
|
||||
this.componentRef = this.#viewContainerRef.createComponent(
|
||||
SkeletonLoaderComponent,
|
||||
);
|
||||
|
||||
untracked(() => {
|
||||
this.applyStyles();
|
||||
});
|
||||
|
||||
this.embeddedViewRef = null; // Clear the embedded view reference
|
||||
} else if (!condition && !this.embeddedViewRef) {
|
||||
// Show original content when condition is false
|
||||
this.#viewContainerRef.clear();
|
||||
this.embeddedViewRef = this.#viewContainerRef.createEmbeddedView(
|
||||
this.#templateRef,
|
||||
);
|
||||
|
||||
this.componentRef = null; // Clear the component reference
|
||||
}
|
||||
});
|
||||
|
||||
updateStyles = effect(() => {
|
||||
this.applyStyles();
|
||||
});
|
||||
|
||||
applyStyles() {
|
||||
if (this.componentRef) {
|
||||
const width = this.uiSkeletonLoaderWidth() || '100%';
|
||||
const height = this.uiSkeletonLoaderHeight() || '100%';
|
||||
const element: HTMLElement | undefined =
|
||||
this.componentRef?.location?.nativeElement;
|
||||
if (element) {
|
||||
element.style.width = width;
|
||||
element.style.height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
libs/ui/skeleton-loader/src/skeleton-loader.scss
Normal file
25
libs/ui/skeleton-loader/src/skeleton-loader.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
.ui-skeleton-loader {
|
||||
@apply inline-block min-h-[1rem] min-w-[2rem] overflow-hidden rounded bg-isa-neutral-300 relative;
|
||||
}
|
||||
|
||||
.ui-skeleton-loader-bar {
|
||||
@keyframes loader {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
@apply block absolute top-0 left-0 right-0 bottom-0;
|
||||
|
||||
@apply bg-gradient-to-r from-isa-neutral-300 via-isa-neutral-400 to-isa-neutral-300;
|
||||
|
||||
animation: loader 1s ease-in-out infinite;
|
||||
}
|
||||
10
libs/ui/skeleton-loader/src/test-setup.ts
Normal file
10
libs/ui/skeleton-loader/src/test-setup.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
|
||||
globalThis.ngJest = {
|
||||
testEnvironmentOptions: {
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
},
|
||||
};
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv();
|
||||
28
libs/ui/skeleton-loader/tsconfig.json
Normal file
28
libs/ui/skeleton-loader/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
17
libs/ui/skeleton-loader/tsconfig.lib.json
Normal file
17
libs/ui/skeleton-loader/tsconfig.lib.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
16
libs/ui/skeleton-loader/tsconfig.spec.json
Normal file
16
libs/ui/skeleton-loader/tsconfig.spec.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
5529
package-lock.json
generated
5529
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -78,11 +78,8 @@
|
||||
"@schematics/angular": "20.0.2",
|
||||
"@softarc/eslint-plugin-sheriff": "^0.18.0",
|
||||
"@softarc/sheriff-core": "0.18.0",
|
||||
"@storybook/angular": "9.0.9",
|
||||
"@storybook/core-server": "8.6.14",
|
||||
"@storybook/jest": "0.2.3",
|
||||
"@storybook/test-runner": "0.23.0",
|
||||
"@storybook/testing-library": "0.2.2",
|
||||
"@storybook/addon-docs": "^9.0.11",
|
||||
"@storybook/angular": "^9.0.5",
|
||||
"@swc-node/register": "1.10.10",
|
||||
"@swc/core": "1.12.1",
|
||||
"@swc/helpers": "0.5.17",
|
||||
@@ -104,13 +101,14 @@
|
||||
"jest-preset-angular": "14.6.0",
|
||||
"jsonc-eslint-parser": "^2.1.0",
|
||||
"ng-mocks": "14.13.5",
|
||||
"ng-packagr": "~18.2.0",
|
||||
"ng-packagr": "^20.0.0",
|
||||
"ng-swagger-gen": "^2.3.1",
|
||||
"nx": "21.2.0",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-url": "~10.1.3",
|
||||
"prettier": "^3.5.2",
|
||||
"pretty-quick": "~4.0.0",
|
||||
"storybook": "^9.0.5",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "10.9.1",
|
||||
|
||||
@@ -75,10 +75,14 @@
|
||||
],
|
||||
"@isa/remission/helpers": ["libs/remission/helpers/src/index.ts"],
|
||||
"@isa/remission/shared": ["libs/remission/shared/src/index.ts"],
|
||||
"@isa/remission/shared/product-info": [
|
||||
"libs/remission/shared/product-info/src/index.ts"
|
||||
"@isa/remission/shared/product-details": [
|
||||
"libs/remission/shared/product-details/src/index.ts"
|
||||
],
|
||||
"@isa/remission/shared/product": [
|
||||
"libs/remission/shared/product/src/index.ts"
|
||||
],
|
||||
"@isa/shared/filter": ["libs/shared/filter/src/index.ts"],
|
||||
"@isa/shared/product-foramt": ["libs/shared/product-format/src/index.ts"],
|
||||
"@isa/shared/product-image": ["libs/shared/product-image/src/index.ts"],
|
||||
"@isa/shared/product-router-link": [
|
||||
"libs/shared/product-router-link/src/index.ts"
|
||||
@@ -95,6 +99,7 @@
|
||||
"@isa/ui/menu": ["libs/ui/menu/src/index.ts"],
|
||||
"@isa/ui/progress-bar": ["libs/ui/progress-bar/src/index.ts"],
|
||||
"@isa/ui/search-bar": ["libs/ui/search-bar/src/index.ts"],
|
||||
"@isa/ui/skeleton-loader": ["libs/ui/skeleton-loader/src/index.ts"],
|
||||
"@isa/ui/toolbar": ["libs/ui/toolbar/src/index.ts"],
|
||||
"@isa/ui/tooltip": ["libs/ui/tooltip/src/index.ts"],
|
||||
"@isa/utils/scroll-position": ["libs/utils/scroll-position/src/index.ts"],
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"extends": "./tsconfig.base.json",
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user