field_type: Any = str
default_value: Optional[Any] = None
optional: bool = False
- html_class: str = ''
- label_html_class: str = 'col-form-label'
- field_html_class: str = 'col-form-input'
def get_type(self):
_type = ''
if self.field_type == str:
_type = 'string'
elif self.field_type == int:
- _type = 'integer'
+ _type = 'int'
elif self.field_type == bool:
_type = 'boolean'
else:
class Container:
def __init__(self, name: str, key: str, fields: List[Union[FormField, "Container"]],
- optional: bool = False, html_class: str = '', label_html_class: str = '',
- field_html_class: str = ''):
+ optional: bool = False, min_items=1):
self.name = name
self.key = key
self.fields = fields
self.optional = optional
- self.html_class = html_class
- self.label_html_class = label_html_class
- self.field_html_class = field_html_class
+ self.min_items = min_items
def layout_type(self):
raise NotImplementedError
properties = None # control schema properties alias
required = None
if self._property_type() == 'array':
+ control_schema['required'] = []
+ control_schema['minItems'] = self.min_items
control_schema['items'] = {
'type': 'object',
'properties': {},
'required': []
}
properties = control_schema['items']['properties']
- required = control_schema['items']['required']
+ required = control_schema['required']
+ control_schema['items']['required'] = required
+
ui_schemas.append({
- 'type': 'array',
'key': key,
- 'htmlClass': self.html_class,
- 'fieldHtmlClass': self.field_html_class,
- 'labelHtmlClass': self.label_html_class,
- 'items': [{
- 'type': 'div',
- 'flex-direction': self.layout_type(),
- 'displayFlex': True,
- 'items': []
- }]
+ 'templateOptions': {
+ 'objectTemplateOptions': {
+ 'layoutType': self.layout_type()
+ }
+ },
+ 'items': []
})
- items = ui_schemas[-1]['items'][0]['items']
+ items = ui_schemas[-1]['items']
else:
control_schema['properties'] = {}
control_schema['required'] = []
required = control_schema['required']
properties = control_schema['properties']
ui_schemas.append({
- 'type': 'section',
- 'flex-direction': self.layout_type(),
- 'displayFlex': True,
- 'htmlClass': self.html_class,
- 'fieldHtmlClass': self.field_html_class,
- 'labelHtmlClass': self.label_html_class,
+ 'templateOptions': {
+ 'layoutType': self.layout_type()
+ },
'key': key,
'items': []
})
properties[field.key]['type'] = _type
properties[field.key]['title'] = field.name
field_ui_schema['key'] = field_key
- field_ui_schema['htmlClass'] = field.html_class
- field_ui_schema['fieldHtmlClass'] = field.field_html_class
- field_ui_schema['labelHtmlClass'] = field.label_html_class
items.append(field_ui_schema)
elif isinstance(field, Container):
container_schema = field.to_dict(key+'.'+field.key if key else field.key)
- control_schema['properties'][field.key] = container_schema['control_schema']
+ properties[field.key] = container_schema['control_schema']
ui_schemas.extend(container_schema['ui_schema'])
if not field.optional:
required.append(field.key)
class Form:
- def __init__(self, path, root_container, action: str = '',
- footer_html_class: str = 'card-footer position-absolute pb-0 mt-3',
- submit_style: str = 'btn btn-primary', cancel_style: str = ''):
+ def __init__(self, path, root_container):
self.path = path
- self.action = action
self.root_container = root_container
- self.footer_html_class = footer_html_class
- self.submit_style = submit_style
- self.cancel_style = cancel_style
def to_dict(self):
- container_schema = self.root_container.to_dict()
-
- # root container style
- container_schema['ui_schema'].append({
- 'type': 'flex',
- 'flex-flow': f'{self.root_container.layout_type()} wrap',
- 'displayFlex': True,
- })
-
- footer = {
- "type": "flex",
- "htmlClass": self.footer_html_class,
- "items": [
- {
- 'type': 'flex',
- 'flex-direction': 'row',
- 'displayFlex': True,
- 'htmlClass': 'd-flex justify-content-end mb-0',
- 'items': [
- {"type": "cancel", "style": self.cancel_style, 'htmlClass': 'mr-2'},
- {"type": "submit", "style": self.submit_style, "title": self.action},
- ]
- }
- ]
- }
- container_schema['ui_schema'].append(footer)
- return container_schema
+ return self.root_container.to_dict()
class CRUDMeta(SerializableClass):
raise DashboardException(msg, code=500)
return f"Successfully created user '{user_entity}'"
-
-create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities',
- label_html_class='hidden cd-header mt-1', fields=[
- FormField('Entity', 'entity',
- field_type=str, html_class='mr-3'),
- FormField('Entity Capabilities',
- 'cap', field_type=str)
- ])
-create_container = VerticalContainer('Create User', 'create_user',
- html_class='d-none', fields=[
- FormField('User entity', 'user_entity',
- field_type=str),
- create_cap_container,
- ])
+create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', fields=[
+ FormField('Entity', 'entity',
+ field_type=str),
+ FormField('Entity Capabilities',
+ 'cap', field_type=str)
+], min_items=1)
+create_container = VerticalContainer('Create User', 'create_user', fields=[
+ FormField('User entity', 'user_entity',
+ field_type=str),
+ create_cap_container,
+])
create_form = Form(path='/cluster/user/create',
- root_container=create_container, action='Create User')
+ root_container=create_container)
@CRUDEndpoint(
"integrity": "sha512-20Pk2Z98fbPLkECcrZSJszKos/OgtvJJR3NcbVfgCJ6EQjDNzW2P1BKqImOz3tJ952dvO2DWEhcLhQ1Wz1e9ng==",
"dev": true
},
- "@ajsf/bootstrap4": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/@ajsf/bootstrap4/-/bootstrap4-0.7.0.tgz",
- "integrity": "sha512-wn6wIQeWknmn/t96XZgihfFq/jjr9GkV9P5dHEU+i9wQbxPNL1MS+x4tLWj9LH3Mx5RiC0Dr4gPgbkDd/bzLxg==",
- "requires": {
- "@ajsf/core": "~0.7.0",
- "lodash-es": "~4.17.21",
- "tslib": "^2.0.0"
- }
- },
- "@ajsf/core": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/@ajsf/core/-/core-0.7.0.tgz",
- "integrity": "sha512-mysKftZAxT0bHYoia7LzbSinK7Z55wINS63zeK/rqSs9r2dF01Vxtzlx2ITViiok3TQ0UV+1OYce/piozEf4aw==",
- "requires": {
- "ajv": "^6.10.0",
- "lodash-es": "~4.17.21",
- "tslib": "^2.0.0"
- },
- "dependencies": {
- "ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
- }
- }
- },
"@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
"integrity": "sha512-wmgOI5sogAuilwBZJqCHVMjm2uhDxjdSmNLFx7eznwGDa6LjvjuATqCv2dVlftq0Y/5oZFVrg5NpyHt5kfZ8Cg==",
"dev": true
},
+ "@ngx-formly/bootstrap": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@ngx-formly/bootstrap/-/bootstrap-6.1.1.tgz",
+ "integrity": "sha512-yNzASqUrzvhMndERzoTBCvj1rtsgsmMXiXsqIP7PRJ4AdGtsTZvpxNYZAltdKEgJvc1hS/lDMJdS7IHg2qFN9Q==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@ngx-formly/core": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.1.1.tgz",
+ "integrity": "sha512-6Fg9TBcXXrnUkHqVlpCQbVE5BWJQBvCitQRngW7kiA/+86rhH5mkL19enULWKq7fEMi54uCVvWsz7l6VOaJhLA==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
},
"fast-glob": {
"version": "3.2.12",
"fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
},
"fast-levenshtein": {
"version": "2.0.6",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
"requires": {
"punycode": "^2.1.0"
},
},
"private": true,
"dependencies": {
- "@ajsf/bootstrap4": "0.7.0",
- "@ajsf/core": "0.7.0",
"@angular/animations": "13.3.11",
"@angular/common": "13.3.11",
"@angular/compiler": "13.3.11",
"@angular/router": "13.3.11",
"@circlon/angular-tree-component": "10.0.0",
"@ng-bootstrap/ng-bootstrap": "12.1.2",
+ "@ngx-formly/bootstrap": "6.1.1",
+ "@ngx-formly/core": "6.1.1",
"@popperjs/core": "2.10.2",
"@swimlane/ngx-datatable": "18.0.0",
"@types/file-saver": "2.0.1",
import { WorkbenchLayoutComponent } from './core/layouts/workbench-layout/workbench-layout.component';
import { ApiDocsComponent } from './core/navigation/api-docs/api-docs.component';
import { ActionLabels, URLVerbs } from './shared/constants/app.constants';
-import { CrudFormComponent } from './shared/datatable/crud-table/crud-form/crud-form.component';
+import { CrudFormComponent } from './shared/forms/crud-form/crud-form.component';
import { CRUDTableComponent } from './shared/datatable/crud-table/crud-table.component';
import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs';
import { AuthGuardService } from './shared/services/auth-guard.service';
+++ /dev/null
-<div class="cd-col-form">
- <div class="card pb-0">
- <div i18n="form title"
- class="card-header">{{ title }}</div>
-
- <div class="card-body position-relative">
- <json-schema-form
- *ngIf="controlSchema && uiSchema"
- [schema]="controlSchema"
- [layout]="uiSchema"
- [data]="data"
- [widgets]="widgets"
- (onSubmit)="submit($event)"
- [options]="formOptions"
- framework="bootstrap-4">
- </json-schema-form>
-
- </div>
- </div>
-</div>
+++ /dev/null
-@use './src/styles/vendor/variables' as vv;
-
-::ng-deep json-schema-form {
- label.control-label.hidden {
- display: none;
- }
-
- .form-group.schema-form-submit p {
- display: none;
- }
-
- legend {
- font-weight: 100 !important;
- }
-
- .card-footer {
- border: 1px solid rgba(0, 0, 0, 0.125);
- left: -1px;
- width: -webkit-fill-available;
- width: -moz-available;
- }
-}
+++ /dev/null
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ToastrModule, ToastrService } from 'ngx-toastr';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
-import { CrudFormComponent } from './crud-form.component';
-import { RouterTestingModule } from '@angular/router/testing';
-
-describe('CrudFormComponent', () => {
- let component: CrudFormComponent;
- let fixture: ComponentFixture<CrudFormComponent>;
- const toastFakeService = {
- error: () => true,
- info: () => true,
- success: () => true
- };
-
- configureTestBed({
- imports: [ToastrModule.forRoot(), RouterTestingModule, HttpClientTestingModule],
- providers: [
- { provide: ToastrService, useValue: toastFakeService },
- { provide: CdDatePipe, useValue: { transform: (d: any) => d } }
- ]
- });
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- declarations: [CrudFormComponent]
- }).compileComponents();
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(CrudFormComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-import { DataGatewayService } from '~/app/shared/services/data-gateway.service';
-import { BackButtonComponent } from '~/app/shared/components/back-button/back-button.component';
-import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
-import { FinishedTask } from '~/app/shared/models/finished-task';
-import { Location } from '@angular/common';
-
-@Component({
- selector: 'cd-crud-form',
- templateUrl: './crud-form.component.html',
- styleUrls: ['./crud-form.component.scss']
-})
-export class CrudFormComponent implements OnInit {
- uiSchema: any;
- controlSchema: any;
- data: any;
- widgets: any = {
- cancel: BackButtonComponent
- };
- resource: string;
- title: string;
-
- formOptions = {
- defautWidgetOptions: {
- validationMessages: {
- required: 'This field is required'
- }
- }
- };
- constructor(
- private dataGatewayService: DataGatewayService,
- private activatedRoute: ActivatedRoute,
- private taskWrapper: TaskWrapperService,
- private location: Location
- ) {}
-
- ngOnInit(): void {
- this.activatedRoute.data.subscribe((data: any) => {
- this.resource = data.resource;
- this.dataGatewayService.list(`ui-${this.resource}`).subscribe((response: any) => {
- this.title = response.forms[0].control_schema.title;
- this.uiSchema = response.forms[0].ui_schema;
- this.controlSchema = response.forms[0].control_schema;
- });
- });
- }
-
- submit(data: any) {
- if (data) {
- this.taskWrapper
- .wrapTaskAroundCall({
- task: new FinishedTask('ceph-user/create', {
- user_entity: data.user_entity
- }),
- call: this.dataGatewayService.create(this.resource, data)
- })
- .subscribe({
- complete: () => {
- this.location.back();
- }
- });
- }
- }
-}
[tableActions]="meta.actions">
</cd-table-actions>
</div>
+ <ng-container *ngIf="expandedRow && meta.detail_columns.length > 0"
+ cdTableDetail>
+ <table class="table table-striped table-bordered">
+ <tbody>
+ <tr *ngFor="let column of meta.detail_columns">
+ <td i18n
+ class="bold">{{ column }}</td>
+ <td> {{ expandedRow[column] }} </td>
+ </tr>
+ </tbody>
+ </table>
+ </ng-container>
</cd-table>
</ng-container>
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { FormlyModule } from '@ngx-formly/core';
+import { FormlyBootstrapModule } from '@ngx-formly/bootstrap';
import { ComponentsModule } from '../components/components.module';
import { PipesModule } from '../pipes/pipes.module';
import { CRUDTableComponent } from './crud-table/crud-table.component';
import { TableKeyValueComponent } from './table-key-value/table-key-value.component';
import { TablePaginationComponent } from './table-pagination/table-pagination.component';
import { TableComponent } from './table/table.component';
-import { Bootstrap4FrameworkModule } from '@ajsf/bootstrap4';
-import { CrudFormComponent } from './crud-table/crud-form/crud-form.component';
+import { CrudFormComponent } from '../forms/crud-form/crud-form.component';
+import { FormlyArrayTypeComponent } from '../forms/crud-form/formly-array-type/formly-array-type.component';
+import { FormlyInputTypeComponent } from '../forms/crud-form/formly-input-type/formly-input-type.component';
+import { FormlyObjectTypeComponent } from '../forms/crud-form/formly-object-type/formly-object-type.component';
@NgModule({
imports: [
PipesModule,
ComponentsModule,
RouterModule,
- Bootstrap4FrameworkModule
+ ReactiveFormsModule,
+ FormlyModule.forRoot({
+ types: [
+ { name: 'array', component: FormlyArrayTypeComponent },
+ { name: 'object', component: FormlyObjectTypeComponent },
+ { name: 'input', component: FormlyInputTypeComponent }
+ ],
+ validationMessages: [{ name: 'required', message: 'This field is required' }]
+ }),
+ FormlyBootstrapModule
],
declarations: [
TableComponent,
TableActionsComponent,
CRUDTableComponent,
TablePaginationComponent,
- CrudFormComponent
+ CrudFormComponent,
+ FormlyArrayTypeComponent,
+ FormlyInputTypeComponent,
+ FormlyObjectTypeComponent
],
exports: [
TableComponent,
--- /dev/null
+<div class="cd-col-form">
+ <div class="card pb-0"
+ *ngIf="formUISchema$ | async as formUISchema">
+ <div i18n="form title"
+ class="card-header">{{ formUISchema.title }}</div>
+ <form *ngIf="formUISchema.uiSchema"
+ [formGroup]="form"
+ (ngSubmit)="submit(model)">
+
+ <div class="card-body position-relative">
+ <formly-form [form]="form"
+ [fields]="formUISchema.controlSchema"
+ [model]="model"
+ [options]="options"></formly-form>
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit(model)"
+ [form]="formDir"
+ [submitText]="formUISchema.title"
+ [disabled]="!form.valid"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </form>
+ </div>
+</div>
--- /dev/null
+@use './src/styles/vendor/variables' as vv;
+
+::ng-deep json-schema-form {
+ label.control-label.hidden {
+ display: none;
+ }
+
+ .form-group.schema-form-submit p {
+ display: none;
+ }
+
+ legend {
+ font-weight: 100 !important;
+ }
+
+ .card-footer {
+ border: 1px solid rgba(0, 0, 0, 0.125);
+ left: -1px;
+ width: -webkit-fill-available;
+ width: -moz-available;
+ }
+}
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule, ToastrService } from 'ngx-toastr';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { CrudFormComponent } from './crud-form.component';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('CrudFormComponent', () => {
+ let component: CrudFormComponent;
+ let fixture: ComponentFixture<CrudFormComponent>;
+ const toastFakeService = {
+ error: () => true,
+ info: () => true,
+ success: () => true
+ };
+
+ configureTestBed({
+ imports: [ToastrModule.forRoot(), RouterTestingModule, HttpClientTestingModule],
+ providers: [
+ { provide: ToastrService, useValue: toastFakeService },
+ { provide: CdDatePipe, useValue: { transform: (d: any) => d } }
+ ]
+ });
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [CrudFormComponent]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CrudFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { DataGatewayService } from '~/app/shared/services/data-gateway.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Location } from '@angular/common';
+import { FormGroup } from '@angular/forms';
+import { FormlyFormOptions } from '@ngx-formly/core';
+import { mergeMap } from 'rxjs/operators';
+import { JsonFormUISchema } from './crud-form.model';
+import { Observable } from 'rxjs';
+
+@Component({
+ selector: 'cd-crud-form',
+ templateUrl: './crud-form.component.html',
+ styleUrls: ['./crud-form.component.scss']
+})
+export class CrudFormComponent implements OnInit {
+ model: any = {};
+ options: FormlyFormOptions = {};
+ resource: string;
+ form = new FormGroup({});
+ formUISchema$: Observable<JsonFormUISchema>;
+
+ constructor(
+ private dataGatewayService: DataGatewayService,
+ private activatedRoute: ActivatedRoute,
+ private taskWrapper: TaskWrapperService,
+ private location: Location
+ ) {}
+
+ ngOnInit(): void {
+ this.formUISchema$ = this.activatedRoute.data.pipe(
+ mergeMap((data: any) => {
+ this.resource = data.resource;
+ return this.dataGatewayService.form(`ui-${this.resource}`);
+ })
+ );
+ }
+
+ submit(data: any) {
+ if (data) {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('ceph-user/create', {
+ user_entity: data.user_entity
+ }),
+ call: this.dataGatewayService.create(this.resource, data)
+ })
+ .subscribe({
+ complete: () => {
+ this.location.back();
+ }
+ });
+ }
+ }
+}
--- /dev/null
+import { FormlyFieldConfig } from '@ngx-formly/core';
+
+export interface JsonFormUISchema {
+ title: string;
+ controlSchema: FormlyFieldConfig[];
+ uiSchema: any;
+}
--- /dev/null
+<div class="mb-3">
+ <legend *ngIf="props.label"
+ class="cd-header mt-1"
+ i18n>{{ props.label }}</legend>
+ <p *ngIf="props.description"
+ i18n>{{ props.description }}</p>
+
+ <div *ngFor="let field of field.fieldGroup; let i = index"
+ class="d-flex">
+ <formly-field class="col"
+ [field]="field"></formly-field>
+ <div class="action-btn">
+ <button class="btn btn-light ms-1"
+ type="button"
+ (click)="addWrapper()">
+ <i [ngClass]="icons.add"></i>
+ </button>
+ <button class="btn btn-light ms-1"
+ type="button"
+ (click)="remove(i)"
+ *ngIf="field.props.removable !== false">
+ <i [ngClass]="icons.trash"></i>
+ </button>
+ </div>
+ </div>
+ <div *ngIf="field.fieldGroup.length === 0"
+ class="text-right">
+ <button class="btn btn-light"
+ type="button"
+ (click)="addWrapper()"
+ i18n>
+ <i [ngClass]="icons.add"></i>
+ Add {{ props.label }}
+ </button>
+ </div>
+
+ <span class="invalid-feedback"
+ role="alert"
+ *ngIf="showError && formControl.errors">
+ <formly-validation-message [field]="field"></formly-validation-message>
+ </span>
+</div>
--- /dev/null
+.action-btn {
+ margin-top: 2.4rem;
+}
--- /dev/null
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyArrayTypeComponent } from './formly-array-type.component';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['input'],
+ defaultValue: {}
+ }
+ ];
+}
+describe('FormlyArrayTypeComponent', () => {
+ let component: MockFormComponent;
+ let fixture: ComponentFixture<MockFormComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [FormlyArrayTypeComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'array', component: FormlyArrayTypeComponent }]
+ })
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+/** Copyright 2021 Formly. All Rights Reserved.
+ Use of this source code is governed by an MIT-style license that
+ can be found in the LICENSE file at https://github.com/ngx-formly/ngx-formly/blob/main/LICENSE */
+
+import { Component, OnInit } from '@angular/core';
+import { FieldArrayType } from '@ngx-formly/core';
+import { forEach } from 'lodash';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-formly-array-type',
+ templateUrl: './formly-array-type.component.html',
+ styleUrls: ['./formly-array-type.component.scss']
+})
+export class FormlyArrayTypeComponent extends FieldArrayType implements OnInit {
+ icons = Icons;
+
+ ngOnInit(): void {
+ this.propagateTemplateOptions();
+ }
+
+ addWrapper() {
+ this.add();
+ this.propagateTemplateOptions();
+ }
+
+ propagateTemplateOptions() {
+ forEach(this.field.fieldGroup, (field) => {
+ if (field.type == 'object') {
+ field.props.templateOptions = this.props.templateOptions.objectTemplateOptions;
+ }
+ });
+ }
+}
--- /dev/null
+<input [formControl]="formControl"
+ class="form-control cd-col-form-input"
+ [formlyAttributes]="field">
--- /dev/null
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyInputTypeComponent } from './formly-input-type.component';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['input'],
+ defaultValue: {}
+ }
+ ];
+}
+
+describe('FormlyInputTypeComponent', () => {
+ let component: MockFormComponent;
+ let fixture: ComponentFixture<MockFormComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [FormlyInputTypeComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'input', component: FormlyInputTypeComponent }]
+ })
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
+
+@Component({
+ selector: 'cd-formly-input-type',
+ templateUrl: './formly-input-type.component.html',
+ styleUrls: ['./formly-input-type.component.scss']
+})
+export class FormlyInputTypeComponent extends FieldType<FieldTypeConfig> {}
--- /dev/null
+<div class="mb-3">
+ <legend *ngIf="props.label"
+ class="cd-col-form-label"
+ i18n>{{ props.label }}</legend>
+ <p *ngIf="props.description"
+ i18n>{{ props.description }}</p>
+ <div class="alert alert-danger"
+ role="alert"
+ *ngIf="showError && formControl.errors">
+ <formly-validation-message [field]="field"></formly-validation-message>
+ </div>
+ <div [ngClass]="inputClass">
+ <formly-field *ngFor="let f of field.fieldGroup"
+ [field]="f"
+ class="flex-grow-1"></formly-field>
+ </div>
+</div>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FormlyObjectTypeComponent } from './formly-object-type.component';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+import { Component } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['object'],
+ defaultValue: {}
+ }
+ ];
+}
+
+describe('FormlyObjectTypeComponent', () => {
+ let fixture: ComponentFixture<MockFormComponent>;
+ let mockComponent: MockFormComponent;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [FormlyObjectTypeComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'object', component: FormlyObjectTypeComponent }]
+ })
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ mockComponent = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(mockComponent).toBeTruthy();
+ });
+});
--- /dev/null
+/** Copyright 2021 Formly. All Rights Reserved.
+ Use of this source code is governed by an MIT-style license that
+ can be found in the LICENSE file at https://github.com/ngx-formly/ngx-formly/blob/main/LICENSE */
+
+import { Component } from '@angular/core';
+import { FieldType } from '@ngx-formly/core';
+
+@Component({
+ selector: 'cd-formly-object-type',
+ templateUrl: './formly-object-type.component.html',
+ styleUrls: ['./formly-object-type.component.scss']
+})
+export class FormlyObjectTypeComponent extends FieldType {
+ get inputClass(): string {
+ const layoutType = this.props.templateOptions?.layoutType;
+ const defaultFlexClasses = 'd-flex justify-content-center align-content-stretch gap-3';
+ if (layoutType == 'row') {
+ return defaultFlexClasses + ' flex-row';
+ }
+ return defaultFlexClasses + ' flex-column';
+ }
+}
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+
+import { CrudFormAdapterService } from './crud-form-adapter.service';
+
+describe('CrudFormAdapterService', () => {
+ let service: CrudFormAdapterService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(CrudFormAdapterService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
--- /dev/null
+import { Injectable } from '@angular/core';
+import { FormlyJsonschema } from '@ngx-formly/core/json-schema';
+import { JsonFormUISchema } from '../forms/crud-form/crud-form.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CrudFormAdapterService {
+ constructor(private formlyJsonschema: FormlyJsonschema) {}
+
+ processJsonSchemaForm(response: any): JsonFormUISchema {
+ const title = response.forms[0].control_schema.title;
+ const uiSchema = response.forms[0].ui_schema;
+ const cSchema = response.forms[0].control_schema;
+ let controlSchema = this.formlyJsonschema.toFieldConfig(cSchema).fieldGroup;
+ for (let i = 0; i < controlSchema.length; i++) {
+ for (let j = 0; j < uiSchema.length; j++) {
+ if (controlSchema[i].key == uiSchema[j].key) {
+ controlSchema[i].className = uiSchema[j].className;
+ controlSchema[i].props.templateOptions = uiSchema[j].templateOptions;
+ }
+ }
+ }
+ return { title, uiSchema, controlSchema };
+ }
+}
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { JsonFormUISchema } from '../forms/crud-form/crud-form.model';
+import { CrudFormAdapterService } from './crud-form-adapter.service';
@Injectable({
providedIn: 'root'
export class DataGatewayService {
cache: { [keys: string]: Observable<any> } = {};
- constructor(private http: HttpClient) {}
+ constructor(private http: HttpClient, private crudFormAdapater: CrudFormAdapterService) {}
list(dataPath: string): Observable<any> {
const cacheable = this.getCacheable(dataPath, 'get');
if (this.cache[cacheable] === undefined) {
- const match = dataPath.match(/(?<url>[^@]+)(?:@(?<version>.+))?/);
- const url = match.groups.url.split('.').join('/');
- const version = match.groups.version || '1.0';
+ const { url, version } = this.getUrlAndVersion(dataPath);
this.cache[cacheable] = this.http.get<any>(url, {
headers: { Accept: `application/vnd.ceph.api.v${version}+json` }
}
create(dataPath: string, data: any): Observable<any> {
- const match = dataPath.match(/(?<url>[^@]+)(?:@(?<version>.+))?/);
- const url = match.groups.url.split('.').join('/');
- const version = match.groups.version || '1.0';
+ const { url, version } = this.getUrlAndVersion(dataPath);
return this.http.post<any>(url, data, {
headers: { Accept: `application/vnd.ceph.api.v${version}+json` }
});
}
+ form(dataPath: string): Observable<JsonFormUISchema> {
+ const cacheable = this.getCacheable(dataPath, 'get');
+ if (this.cache[cacheable] === undefined) {
+ const { url, version } = this.getUrlAndVersion(dataPath);
+
+ this.cache[cacheable] = this.http.get<any>(url, {
+ headers: { Accept: `application/vnd.ceph.api.v${version}+json` }
+ });
+ }
+ return this.cache[cacheable].pipe(
+ map((response) => {
+ return this.crudFormAdapater.processJsonSchemaForm(response);
+ })
+ );
+ }
+
getCacheable(dataPath: string, method: string) {
return dataPath + method;
}
+
+ getUrlAndVersion(dataPath: string) {
+ const match = dataPath.match(/(?<url>[^@]+)(?:@(?<version>.+))?/);
+ const url = match.groups.url.split('.').join('/');
+ const version = match.groups.version || '1.0';
+
+ return { url: url, version: version };
+ }
}
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
-import { Bootstrap4FrameworkModule } from '@ajsf/bootstrap4';
+import { ReactiveFormsModule } from '@angular/forms';
+import { FormlyModule } from '@ngx-formly/core';
+import { FormlyBootstrapModule } from '@ngx-formly/bootstrap';
import { CssHelper } from '~/app/shared/classes/css-helper';
import { ComponentsModule } from './components/components.module';
import { AuthGuardService } from './services/auth-guard.service';
import { AuthStorageService } from './services/auth-storage.service';
import { FormatterService } from './services/formatter.service';
+import { FormlyArrayTypeComponent } from './forms/crud-form/formly-array-type/formly-array-type.component';
+import { FormlyObjectTypeComponent } from './forms/crud-form/formly-object-type/formly-object-type.component';
+import { FormlyInputTypeComponent } from './forms/crud-form/formly-input-type/formly-input-type.component';
@NgModule({
imports: [
ComponentsModule,
DataTableModule,
DirectivesModule,
- Bootstrap4FrameworkModule
+
+ ReactiveFormsModule,
+ FormlyModule.forRoot({
+ types: [
+ { name: 'array', component: FormlyArrayTypeComponent },
+ { name: 'object', component: FormlyObjectTypeComponent },
+ { name: 'input', component: FormlyInputTypeComponent }
+ ],
+ validationMessages: [{ name: 'required', message: 'This field is required' }]
+ }),
+ FormlyBootstrapModule
],
declarations: [],
exports: [ComponentsModule, PipesModule, DataTableModule, DirectivesModule],
@extend .badge, .bg-dark;
}
-json-schema-form {
- .help-block {
- @extend .invalid-feedback;
- }
-
+formly-form {
.ng-touched.ng-invalid {
@extend .is-invalid;
}
.ng-touched.ng-valid {
@extend .is-valid;
}
+
+ .form-label {
+ @extend .cd-col-form-label;
+ text-align: start;
+ width: 50%;
+
+ span[aria-hidden='true'] {
+ color: $danger;
+ }
+ }
}
def test_create_form(self):
form_dict = create_form.to_dict()
schema = {'schema': form_dict['control_schema'], 'layout': form_dict['ui_schema']}
- validate(instance={'user_entity': 'foo', 'capabilities': []}, schema=schema['schema'])
+ validate(instance={'user_entity': 'foo',
+ 'capabilities': [{"entity": "mgr", "cap": "allow *"}]},
+ schema=schema['schema'])
root_container=VerticalContainer('Create user', key='create_user', fields=[
FormField('User entity', key='user_entity', field_type=str),
ArrayHorizontalContainer('Capabilities', key='caps', fields=[
- FormField('left', field_type=str, key='left',
- html_class='cd-col-form-input'),
+ FormField('left', field_type=str, key='left'),
FormField('right', key='right', field_type=str)
]),
ArrayVerticalContainer('ah', key='ah', fields=[
- FormField('top', key='top', field_type=str, label_html_class='d-none'),
+ FormField('top', key='top', field_type=str),
FormField('bottom', key='bottom', field_type=str)
]),
HorizontalContainer('oh', key='oh', fields=[
- FormField('left', key='left', field_type=str, label_html_class='d-none'),
+ FormField('left', key='left', field_type=str),
FormField('right', key='right', field_type=str)
]),
VerticalContainer('ov', key='ov', fields=[
- FormField('top', key='top', field_type=str, label_html_class='d-none'),
+ FormField('top', key='top', field_type=str),
FormField('bottom', key='bottom', field_type=bool)
]),
]))