CM-42: Init Core Lib: Dialog & Actions module (#2)

This commit is contained in:
Rinat Arsaev 2023-04-18 22:45:35 +04:00 committed by GitHub
parent 3e92c80b26
commit 2940aee23b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 7873 additions and 83 deletions

View File

@ -1,16 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

21
.github/workflows/main.yaml vendored Normal file
View File

@ -0,0 +1,21 @@
name: 'Main'
on:
push:
branches: ['master', 'main']
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: valitydev/action-frontend/setup@v0.1
- run: npm ci
- name: Build
run: npm run build
- uses: valitydev/action-frontend/publish@v0.1
with:
npm-token: ${{ secrets.NPM_TOKEN }}
directory: ./dist/ng-core
version-up: 'false'

18
.github/workflows/pr.yaml vendored Normal file
View File

@ -0,0 +1,18 @@
name: 'PR'
on:
pull_request:
branches: ['*']
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: valitydev/action-frontend/setup@v0.1
- run: npm ci
- name: Format
run: npm run format:check
- name: Build
run: npm run build

2
.gitignore vendored
View File

@ -7,7 +7,7 @@
/bazel-out
# Node
/node_modules
node_modules
npm-debug.log
yarn-error.log

8
.prettierignore Normal file
View File

@ -0,0 +1,8 @@
package.json
package-lock.json
node_modules
dist
.angular
Dockerfile
*.conf

24
.prettierrc Normal file
View File

@ -0,0 +1,24 @@
{
"printWidth": 100,
"singleQuote": true,
"tabWidth": 4,
"attributeSort": "ASC",
"attributeGroups": [
"$ANGULAR_ELEMENT_REF",
"$ANGULAR_STRUCTURAL_DIRECTIVE",
"$ANGULAR_ANIMATION",
"$ANGULAR_ANIMATION_INPUT",
"$ANGULAR_TWO_WAY_BINDING",
"$ANGULAR_INPUT",
"$DEFAULT",
"$ANGULAR_OUTPUT"
],
"overrides": [
{
"files": ["{.vscode,.github}/**"],
"options": {
"tabWidth": 2
}
}
]
}

View File

@ -1,27 +1,3 @@
# NgCore
# Angular Libraries
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.2.5.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
- [Core](/projects/ng-core/README.md)

View File

@ -3,5 +3,35 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"ng-core": {
"projectType": "library",
"root": "projects/ng-core",
"sourceRoot": "projects/ng-core/src",
"prefix": "v",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/ng-core/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/ng-core/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/ng-core/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "projects/ng-core/tsconfig.spec.json",
"polyfills": ["zone.js", "zone.js/testing"]
}
}
}
}
}
}

7316
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,19 @@
{
"name": "ng-core",
"name": "ng-libs",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"start": "npm run watch",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "ng test",
"format": "prettier * --write --loglevel warn",
"format:check": "prettier * --list-different"
},
"private": true,
"workspaces": [
"./projects/*"
],
"dependencies": {
"@angular/animations": "^15.2.0",
"@angular/common": "^15.2.0",
@ -23,6 +28,7 @@
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.6",
"@angular/cli": "~15.2.5",
"@angular/compiler-cli": "^15.2.0",
"@types/jasmine": "~4.3.0",
@ -32,6 +38,9 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"ng-packagr": "^15.2.2",
"prettier": "^2.8.7",
"prettier-plugin-organize-attributes": "^0.0.5",
"typescript": "~4.9.4"
}
}

View File

@ -0,0 +1 @@
# Angular Core

View File

@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/ng-core",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@ -0,0 +1,16 @@
{
"name": "@vality/ng-core",
"version": "0.0.1",
"peerDependencies": {
"@angular/cdk": ">=15.0.0",
"@angular/common": ">=15.0.0",
"@angular/core": ">=15.0.0",
"@angular/material": ">=15.0.0",
"coerce-property": ">=0.3.2",
"utility-types": ">=3.0.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}

View File

@ -0,0 +1,3 @@
<div class="v-actions">
<ng-content></ng-content>
</div>

View File

@ -0,0 +1,26 @@
::ng-deep .v-actions {
box-sizing: border-box;
display: flex;
place-content: center space-between;
align-items: center;
flex: 1 1 100%;
max-width: 100%;
flex-direction: row;
@media screen and (max-width: 959px) {
flex-direction: column;
}
& > *:first-child:not(:last-child) {
margin-right: auto !important;
@media screen and (max-width: 959px) {
margin-right: initial !important;
}
}
& > *:first-child:last-child {
margin-left: auto !important;
@media screen and (max-width: 959px) {
margin-left: initial !important;
}
}
}

View File

@ -0,0 +1,9 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'v-actions',
templateUrl: 'actions.component.html',
styleUrls: ['actions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActionsComponent {}

View File

@ -0,0 +1,10 @@
import { NgModule } from '@angular/core';
import { ActionsComponent } from './actions.component';
@NgModule({
imports: [],
declarations: [ActionsComponent],
exports: [ActionsComponent],
})
export class ActionsModule {}

View File

@ -0,0 +1,2 @@
export * from './actions.module';
export * from './actions.component';

View File

@ -0,0 +1 @@
<v-actions><ng-content></ng-content></v-actions>

View File

@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'v-dialog-actions',
templateUrl: './dialog-actions.component.html',
})
export class DialogActionsComponent {}

View File

@ -0,0 +1,34 @@
<div class="dialog">
<div class="dialog-title">
<div>
<h2 class="mat-h2">{{ title }}</h2>
</div>
<mat-icon
[matDialogClose]="cancelData"
class="dialog-title-close"
inline
(click)="cancelDialog()"
>clear</mat-icon
>
</div>
<ng-container *ngIf="!noContent">
<mat-divider *ngIf="hasDivider"></mat-divider>
<div class="dialog-content">
<ng-content></ng-content>
</div>
</ng-container>
<ng-container *ngIf="!noActions">
<mat-divider *ngIf="hasDivider"></mat-divider>
<div class="dialog-actions">
<ng-content select="v-dialog-actions"></ng-content>
</div>
</ng-container>
<mat-progress-bar
*ngIf="inProgress || progress"
[mode]="inProgress ? 'indeterminate' : 'determinate'"
[value]="progress"
></mat-progress-bar>
</div>

View File

@ -0,0 +1,63 @@
$base-padding: 24px;
$max-height-mobile: 100vh;
$max-height-desktop: 90vh;
:host {
display: flex;
flex-direction: column;
flex-grow: 1;
height: 100%;
}
.dialog {
flex-direction: column;
box-sizing: border-box;
display: flex;
flex: 1 1 100%;
max-height: calc(#{$max-height-desktop} - #{$base-padding * 2}) !important;
@media screen and (max-width: 959px) {
max-height: calc(#{$max-height-mobile} - #{$base-padding * 2}) !important;
}
&-title {
flex-direction: row;
box-sizing: border-box;
display: flex;
place-content: center space-between;
align-items: center;
padding: $base-padding;
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
}
&-close {
&#{&} {
cursor: pointer;
font-size: 24px;
&:hover {
opacity: 0.5;
}
}
}
}
&-content {
// instead of flex="grow". Don't need wrong rule "max-height": 100% to support scroll content
flex: 1 1 100%;
box-sizing: border-box;
padding: $base-padding;
overflow: auto;
}
&-actions {
padding: $base-padding;
}
}

View File

@ -0,0 +1,32 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { coerceBoolean } from 'coerce-property';
import { DialogResponseStatus } from './types/dialog-response-status';
@Component({
selector: 'v-dialog',
templateUrl: 'dialog.component.html',
styleUrls: ['dialog.component.scss'],
})
export class DialogComponent {
@Input() title!: string;
@coerceBoolean @Input() disabled = false;
@coerceBoolean @Input() inProgress = false;
@Input() progress?: number;
@coerceBoolean @Input() hasDivider = true;
@coerceBoolean @Input() noContent = false;
@coerceBoolean @Input() noActions = false;
@Output() cancel = new EventEmitter<void>();
cancelData = {
status: DialogResponseStatus.Cancelled,
};
cancelDialog(): void {
this.cancel.emit();
}
}

View File

@ -0,0 +1,30 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { ActionsModule } from '../actions';
import { DialogComponent } from './dialog.component';
import { DialogActionsComponent } from './components/dialog-actions/dialog-actions.component';
import { DialogService } from './services/dialog.service';
const SHARED_DECLARATIONS = [DialogComponent, DialogActionsComponent];
@NgModule({
imports: [
CommonModule,
MatDividerModule,
MatButtonModule,
ActionsModule,
MatIconModule,
MatProgressBarModule,
MatDialogModule,
],
providers: [DialogService],
declarations: SHARED_DECLARATIONS,
exports: SHARED_DECLARATIONS,
})
export class DialogModule {}

View File

@ -0,0 +1,8 @@
export * from './dialog.module';
export * from './types/dialog-response-status';
export * from './types/dialog-response';
export * from './utils/dialog-superclass';
export * from './tokens';
export * from './dialog.component';
export * from './components/dialog-actions/dialog-actions.component';
export * from './services/dialog.service';

View File

@ -0,0 +1,45 @@
import { ComponentType } from '@angular/cdk/overlay';
import { Inject, Injectable, Optional } from '@angular/core';
import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { DEFAULT_DIALOG_CONFIG, DIALOG_CONFIG, DialogConfig } from '../tokens';
import { DialogResponse } from '../types/dialog-response';
import { DialogSuperclass } from '../utils/dialog-superclass';
@Injectable({
providedIn: 'root',
})
export class DialogService {
constructor(
private dialog: MatDialog,
@Optional()
@Inject(DIALOG_CONFIG)
private readonly dialogConfig: DialogConfig
) {
if (!dialogConfig) this.dialogConfig = DEFAULT_DIALOG_CONFIG;
}
open<C, D, R, S>(
dialogComponent: ComponentType<DialogSuperclass<C, D, R, S>>,
/**
* Workaround when both conditions for the 'data' argument must be true:
* - typing did not require passing when it is optional (for example: {param: number} | void)
* - typing required to pass when it is required (for example: {param: number})
*/
...[data, configOrConfigName]: D extends void
? []
: [data: D, configOrConfigName?: Omit<MatDialogConfig<D>, 'data'> | keyof DialogConfig]
): MatDialogRef<C, DialogResponse<R, S>> {
let config: Partial<MatDialogConfig<D>>;
if (!configOrConfigName) config = {};
else if (typeof configOrConfigName === 'string')
config = this.dialogConfig[configOrConfigName];
else config = configOrConfigName;
return this.dialog.open(dialogComponent as never, {
data,
...(dialogComponent as typeof DialogSuperclass).defaultDialogConfig,
...config,
});
}
}

View File

@ -0,0 +1,20 @@
import { InjectionToken } from '@angular/core';
import { MatDialogConfig } from '@angular/material/dialog';
import { ValuesType } from 'utility-types';
export type DialogConfig = Record<'small' | 'medium' | 'large', MatDialogConfig<undefined>>;
export const DIALOG_CONFIG = new InjectionToken<DialogConfig>('dialogConfig');
export const BASE_CONFIG: ValuesType<DialogConfig> = {
maxHeight: '90vh',
disableClose: true,
autoFocus: false,
width: '552px',
};
export const DEFAULT_DIALOG_CONFIG: DialogConfig = {
small: { ...BASE_CONFIG, width: '360px' },
medium: BASE_CONFIG,
large: { ...BASE_CONFIG, width: '800px' },
};

View File

@ -0,0 +1,5 @@
export enum DialogResponseStatus {
Success = 'success',
Error = 'error',
Cancelled = 'canceled',
}

View File

@ -0,0 +1,7 @@
import { DialogResponseStatus } from './dialog-response-status';
export interface DialogResponse<T = void, S = void> {
status: S | DialogResponseStatus;
data?: T;
error?: unknown;
}

View File

@ -0,0 +1,43 @@
import { Directive, Injector } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DEFAULT_DIALOG_CONFIG } from '../tokens';
import { DialogResponse as TDialogResponse } from '../types/dialog-response';
import { DialogResponseStatus } from '../types/dialog-response-status';
@Directive()
export class DialogSuperclass<
DialogComponent,
DialogData = void,
DialogResponseData = void,
DialogResponseStatus = void,
DialogResponse = TDialogResponse<DialogResponseData, DialogResponseStatus>
> {
static defaultDialogConfig = DEFAULT_DIALOG_CONFIG.medium;
dialogData: DialogData = this.injector.get(MAT_DIALOG_DATA) as DialogData;
dialogRef = this.injector.get(MatDialogRef) as MatDialogRef<DialogComponent, DialogResponse>;
constructor(private injector: Injector) {}
closeWithCancellation(data?: DialogResponseData) {
this.dialogRef.close({
status: DialogResponseStatus.Cancelled,
data,
} as never);
}
closeWithSuccess(data?: DialogResponseData) {
this.dialogRef.close({
status: DialogResponseStatus.Success,
data,
} as never);
}
closeWithError(data?: DialogResponseData) {
this.dialogRef.close({
status: DialogResponseStatus.Error,
data,
} as never);
}
}

View File

@ -0,0 +1,2 @@
export * from './dialog';
export * from './actions';

View File

@ -0,0 +1 @@
export * from './components';

View File

@ -0,0 +1,5 @@
/*
* Public API Surface of ng-core
*/
export * from './lib';

View File

@ -0,0 +1,12 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": ["**/*.spec.ts"]
}

View File

@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@ -0,0 +1,9 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": ["jasmine"]
},
"include": ["**/*.spec.ts", "**/*.d.ts"]
}

View File

@ -11,6 +11,9 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"paths": {
"ng-core": ["dist/ng-core"]
},
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
@ -19,10 +22,7 @@
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
"lib": ["ES2022", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,