Skip to content

Commit

Permalink
feat(form): add acl property (#574)
Browse files Browse the repository at this point in the history
  • Loading branch information
cipchk committed May 22, 2019
1 parent d3dd0e4 commit 54ddae9
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 26 deletions.
8 changes: 4 additions & 4 deletions packages/form/docs/getting-started.zh-CN.md
Expand Up @@ -109,10 +109,10 @@ export class DelonModule {

```ts
@Component({
selector: 'app-home',
template: `
<sf [schema]="schema" (formSubmit)="submit($event)"></sf>
`
selector: 'app-home',
template: `
<sf [schema]="schema" (formSubmit)="submit($event)"></sf>
`
})
export class HomeComponent {
schema: SFSchema = {
Expand Down
15 changes: 8 additions & 7 deletions packages/form/docs/schema.en-US.md
Expand Up @@ -205,13 +205,14 @@ Equals to `<sf [ui]="ui">`, a group of UI structure corresponds to JSON Schema s

### Basic Type

| Parameter | Description | Type | Default Value | |
|-----------------|----------------------|--------------------------------------------------------|--------|---|
| `[debug]` | Debug mode | `boolean` | - | |
| `[order]` | Order of property | `string[]` | - | |
| `[asyncData]` | Asynchronized static data source | `(input?: any) => Observable<SFSchemaEnumType[]>` | - | |
| `[hidden]` | If hide | `boolean` | `false` | |
| `[visibleIf]` | Is visible with conditions | `{ [key: string]: any[] | ((value: any) => boolean) }` | - | |
| Parameter | Description | Type | Default Value |
|---------------|-----------------|--------------------------|---------------|
| `[debug]` | Debug mode | `boolean` | - |
| `[order]` | Order of property | `string[]` | - |
| `[asyncData]` | Asynchronized static data source | `(input?: any) => Observable<SFSchemaEnumType[]>` | - |
| `[hidden]` | If hide | `boolean` | `false` | |
| `[visibleIf]` | Is visible with conditions | `{ [key: string]: any[] | ((value: any) => boolean) }` | - |
| `[acl]` | ACL permission (Use `can()` verify) | `ACLCanType` | - |

**visibleIf**

Expand Down
15 changes: 8 additions & 7 deletions packages/form/docs/schema.zh-CN.md
Expand Up @@ -205,13 +205,14 @@ UI Schema 结构由通用性和小部件API两部分组成,以下是通用性

### 基础类

| 参数 | 说明 | 类型 | 默认值 | |
|-----------------|----------------------|--------------------------------------------------------|--------|---|
| `[debug]` | 调试模式 | `boolean` | - | |
| `[order]` | 属性顺序 | `string[]` | - | |
| `[asyncData]` | 异步静态数据源 | `(input?: any) => Observable<SFSchemaEnumType[]>` | - | |
| `[hidden]` | 是否隐藏渲染 | `boolean` | `false` | |
| `[visibleIf]` | 指定条件时才显示 | `{ [key: string]: any[] | ((value: any) => boolean) }` | - | |
| 参数 | 说明 | 类型 | 默认值 |
|-----------------|----------------------|----------------------------------|--------|
| `[debug]` | 调试模式 | `boolean` | - |
| `[order]` | 属性顺序 | `string[]` | - |
| `[asyncData]` | 异步静态数据源 | `(input?: any) => Observable<SFSchemaEnumType[]>` | - |
| `[hidden]` | 是否隐藏渲染 | `boolean` | `false` |
| `[visibleIf]` | 指定条件时才显示 | `{ [key: string]: any[] | ((value: any) => boolean) }` | - |
| `[acl]` | ACL权限,等同 `can()` 参数值 | `ACLCanType` | - |

**visibleIf**

Expand Down
58 changes: 58 additions & 0 deletions packages/form/examples/acl/demo/simple.md
@@ -0,0 +1,58 @@
---
title:
zh-CN: 基础样例
en-US: Basic Usage
order: 0
---

## zh-CN

最简单的用法。

## en-US

Simplest of usage.

```ts
import { Component } from '@angular/core';
import { ACLService } from '@delon/acl';
import { SFSchema } from '@delon/form';
import { NzMessageService } from 'ng-zorro-antd';

@Component({
selector: 'app-demo',
template: `
<sf [schema]="schema" (formSubmit)="submit($event)"></sf>
<button nz-button nzType="primary" (click)="acl.setFull(true)">Full</button>
<button nz-button nzType="primary" (click)="acl.setFull(false)">Not Full</button>
<button nz-button nzType="primary" (click)="acl.setRole(['admin'])">Admin Role</button>
<button nz-button nzType="primary" (click)="acl.setRole(['user'])">User Role</button>
`,
})
export class DemoComponent {
schema: SFSchema = {
properties: {
name: {
type: 'string',
title: 'name-user',
ui: {
acl: 'user',
},
},
age: {
type: 'string',
title: 'age-admin',
ui: {
acl: 'admin',
},
},
},
required: ['name'],
};
constructor(public msg: NzMessageService, public acl: ACLService) {
}
submit(value: any) {
this.msg.success(JSON.stringify(value));
}
}
```
7 changes: 7 additions & 0 deletions packages/form/examples/acl/index.md
@@ -0,0 +1,7 @@
---
title: acl
subtitle: ACL
type: Examples
---

结合 `@delon/acl` 权限可以利用一个 Schema 来构建不同角色或权限点的表单。
1 change: 1 addition & 0 deletions packages/form/ng-package.json
Expand Up @@ -31,6 +31,7 @@
"ng-zorro-antd/tree-select": "ng-zorro-antd/tree-select",
"ng-zorro-antd/upload": "ng-zorro-antd/upload",
"@delon/theme": "delon.theme",
"@delon/acl": "delon.acl",
"@delon/util": "delon.util"
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/form/package.json
Expand Up @@ -29,6 +29,7 @@
],
"peerDependencies": {
"@delon/theme": "PEER-0.0.0-PLACEHOLDER",
"@delon/acl": "PEER-0.0.0-PLACEHOLDER",
"@delon/util": "PEER-0.0.0-PLACEHOLDER"
},
"sideEffects": false
Expand Down
41 changes: 41 additions & 0 deletions packages/form/spec/form.spec.ts
@@ -1,6 +1,7 @@
import { Component, DebugElement } from '@angular/core';
import { fakeAsync, tick, ComponentFixture, TestBed, TestBedStatic } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ACLService } from '@delon/acl';
import { configureTestSuite, createTestContext } from '@delon/testing';
import { en_US, AlainThemeModule, DelonLocaleService } from '@delon/theme';
import { deepCopy } from '@delon/util';
Expand Down Expand Up @@ -125,6 +126,20 @@ describe('form: component', () => {
fixture.detectChanges();
expect(console.warn).toHaveBeenCalled();
});

it('should be ingore required when element is hidden', () => {
const s: SFSchema = {
properties: {
name: {
type: 'string',
ui: { hidden: true },
},
},
required: ['name'],
};
page.newSchema(s);
expect(context.comp._schema.required!.indexOf('name') === -1).toBe(true);
});
});

describe('[button]', () => {
Expand Down Expand Up @@ -541,6 +556,32 @@ describe('form: component', () => {
expect((s.properties!.a.ui as any).errors.required).toHaveBeenCalled();
});
});

describe('ACL', () => {
let acl: ACLService;
beforeEach(() => (acl = injector.get(ACLService)));
it('shoule be working', fakeAsync(() => {
acl.setFull(false);
acl.setRole(['admin']);
const s: SFSchema = {
properties: {
a: {
type: 'string',
ui: {
acl: 'admin',
},
},
},
required: ['a'],
};
page.newSchema(s);
page.checkUI('/a', 'hidden', false);
acl.setRole(['user']);
tick();
fixture.detectChanges();
page.checkUI('/a', 'hidden', true);
}));
});
});

describe('#mode', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/form/src/interface.ts
Expand Up @@ -2,6 +2,8 @@ import { SFHorizontalLayoutSchema, SFRenderSchema } from './schema/ui';

export type SFValue = any;

export type SFLayout = 'horizontal' | 'vertical' | 'inline';

export interface SFButton {
/** 提交按钮文本,默认:`提交` */
submit?: string;
Expand Down
7 changes: 6 additions & 1 deletion packages/form/src/schema/ui.ts
@@ -1,6 +1,6 @@
import { TemplateRef } from '@angular/core';
import { ACLCanType } from '@delon/acl';
import { Observable } from 'rxjs';

import { ErrorSchema } from '../errors';
import { SFSchemaEnumType } from './index';

Expand Down Expand Up @@ -172,6 +172,11 @@ export interface SFUISchemaItem
* - `visibleIf: { shown: (value: any) => value > 0 }`:复杂表达式
*/
visibleIf?: { [key: string]: any[] | ((value: any) => boolean) };

/**
* ACL 配置
*/
acl?: ACLCanType;
}

/**
Expand Down
37 changes: 30 additions & 7 deletions packages/form/src/sf.component.ts
Expand Up @@ -7,19 +7,21 @@ import {
OnChanges,
OnDestroy,
OnInit,
Optional,
Output,
SimpleChange,
SimpleChanges,
TemplateRef,
ViewEncapsulation,
} from '@angular/core';
import { ACLService } from '@delon/acl';
import { DelonLocaleService, LocaleData } from '@delon/theme';
import { deepCopy, InputBoolean } from '@delon/util';
import { Subscription } from 'rxjs';

import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { DelonFormConfig } from './config';
import { ErrorData } from './errors';
import { SFButton } from './interface';
import { SFButton, SFLayout } from './interface';
import { FormProperty } from './model/form.property';
import { FormPropertyFactory } from './model/form.property.factory';
import { SFSchema } from './schema/index';
Expand Down Expand Up @@ -58,7 +60,7 @@ export function useFactory(schemaValidatorFactory: SchemaValidatorFactory, optio
encapsulation: ViewEncapsulation.None,
})
export class SFComponent implements OnInit, OnChanges, OnDestroy {
private i18n$: Subscription;
private unsubscribe$ = new Subject<void>();
private _renders = new Map<string, TemplateRef<void>>();
private _item: {};
private _valid = true;
Expand All @@ -75,7 +77,7 @@ export class SFComponent implements OnInit, OnChanges, OnDestroy {
// #region fields

/** 表单布局,等同 `nzLayout`,默认:horizontal */
@Input() layout: 'horizontal' | 'vertical' | 'inline' = 'horizontal';
@Input() layout: SFLayout = 'horizontal';
/** JSON Schema */
@Input() schema: SFSchema;
/** UI Schema */
Expand Down Expand Up @@ -194,19 +196,26 @@ export class SFComponent implements OnInit, OnChanges, OnDestroy {
private formPropertyFactory: FormPropertyFactory,
private terminator: TerminatorService,
private options: DelonFormConfig,
@Optional() private aclSrv: ACLService,
private cdr: ChangeDetectorRef,
private i18n: DelonLocaleService,
) {
this.liveValidate = options.liveValidate as boolean;
this.firstVisual = options.firstVisual as boolean;
this.autocomplete = options.autocomplete as 'on' | 'off';
this.i18n$ = this.i18n.change.subscribe(() => {
this.i18n.change.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
this.locale = this.i18n.getData('sf');
if (this._inited) {
this.coverButtonProperty();
this.cdr.detectChanges();
}
});
this.aclSrv.change
.pipe(
filter(() => this._inited),
takeUntil(this.unsubscribe$),
)
.subscribe(() => this.refreshSchema());
}

private coverProperty() {
Expand All @@ -221,6 +230,8 @@ export class SFComponent implements OnInit, OnChanges, OnDestroy {
parentUiSchema: SFUISchemaItemRun,
uiRes: SFUISchemaItemRun,
) => {
if (!Array.isArray(schema.required)) schema.required = [];

Object.keys(schema.properties!).forEach(key => {
const uiKey = `$${key}`;
const property = retrieveSchema(schema.properties![key] as SFSchema, definitions);
Expand Down Expand Up @@ -267,10 +278,20 @@ export class SFComponent implements OnInit, OnChanges, OnDestroy {
}
}
ui.hidden = typeof ui.hidden === 'boolean' ? ui.hidden : false;
if (ui.hidden === false && ui.acl && this.aclSrv && !this.aclSrv.can(ui.acl)) {
ui.hidden = true;
}

uiRes[uiKey] = ui;
delete property.ui;

if (ui.hidden === true) {
const idx = schema.required!.indexOf(key);
if (idx !== -1) {
schema.required!.splice(idx, 1);
}
}

if (property.items) {
uiRes[uiKey].$items = uiRes[uiKey].$items || {};
inFn(property.items, property.items, (uiSchema[uiKey] || {}).$items || {}, ui, uiRes[uiKey].$items);
Expand Down Expand Up @@ -460,6 +481,8 @@ export class SFComponent implements OnInit, OnChanges, OnDestroy {
ngOnDestroy(): void {
this.cleanRootSub();
this.terminator.destroy();
this.i18n$.unsubscribe();
const { unsubscribe$ } = this;
unsubscribe$.next();
unsubscribe$.complete();
}
}

0 comments on commit 54ddae9

Please sign in to comment.