Custom mass actions with dialog in OroCRM - widget

I am making some customizations to OroCRM, in this case I need to do a massive action on several records, but when selecting the desired option a modal should appear to choose one of the possible owners of the system.
For that I included the following changes in the system:
I included the mass-actions option in the datagrids:
change_owner:
type: lead_change_owner_mass_edit
handler: pb_lead.mass_action.change_owner.handler
acl_resource: oro_sales_lead_update
route: pb_lead_datagrid_mass_action_change_owner
label: pb.lead.mass_actions.change_owner.label
icon: user
data_identifier: lead.id
frontend_options:
title: "#translator->trans(pb.lead.mass_actions.change_owner.label)"
dialogOptions:
width: 500
modal: true
allowMaximize: false
allowMinimize: false
I defined an associated action as shown:
class ChangeOwnerAction extends WindowMassAction
{
/** #var array */
protected $requiredOptions = ['handler', 'route', 'data_identifier'];
/**
* {#inheritDoc}
*/
public function setOptions(ActionConfiguration $options)
{
if (empty($options['frontend_type'])) {
$options['frontend_type'] = 'edit-mass';
}
return parent::setOptions($options);
}
/**
* {#inheritdoc}
*/
protected function getAllowedRequestTypes()
{
return [Request::METHOD_POST];
}
}
This action is defined as a service:
services:
#Change owner windows action
pb_lead.mass_action.type.changeownermass:
class: PB\Bundle\LeadBundle\Datagrid\Extension\MassAction\Actions\Widget\ChangeOwnerAction
shared: false
tags:
- { name: oro_datagrid.extension.mass_action.type, type: lead_change_owner_mass_edit }
Additionally, the controller that has a method with the route defined in the datagrid is included:
/**
* #Route("/change-owner-mass-edit-lead", name="pb_lead_datagrid_mass_action_change_owner")
* #AclAncestor("oro_sales_lead_update")
* #Template("#PBLead/Lead/widget/mass_change_owner_update.html.twig")
* #param Request $request
* #return array
*/
public function massChangeOwnerAction(Request $request)
{
dump($request);
$responseData = [
'inset' => $request->get('inset', null),
'values' => $request->get('values', null),
];
...
return $responseData;
}
This controller was defined as a service in controllers.yml:
PB\Bundle\LeadBundle\Controller\Frontend\LeadChangeOwnerController:
calls:
- [setContainer, ['#Psr\Container\ContainerInterface']]
tags:
- { name: container.service_subscriber }
On the other hand, 2 classes were defined: an abstract and a handler that extends from it to manage the mass actions. Both handlers are defined as services:
#Abstract Lead mass action handler
pb_lead.abstract_lead_mass_action_handler:
class: PB\Bundle\LeadBundle\Datagrid\Extension\MassAction\AbstractLeadMassActionHandler
abstract: true
arguments:
- '#oro_entity.doctrine_helper'
- '#oro_security.acl_helper'
#Lead change owner mass action HANDLER
pb_lead.mass_action.change_owner.handler:
parent: pb_lead.abstract_lead_mass_action_handler
public: true
class: PB\Bundle\LeadBundle\Datagrid\Extension\MassActionxzczxcz\LeadChangeOwnerMassActionHandler
calls:
- ['setTranslator', ['#translator']]
- ['setFormFactory', ['#form.factory']]
There is a custom form and a view that are called from the controller.
Form:
class LeadChangeOwnerMassType extends AbstractType
{
const NAME = 'pb_lead_change_owner_mass_type';
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'owner',
UserAclSelectType::class,
[
'required' => true,
'label' => 'pb.lead.mass_actions.change_owner.label',
'constraints' => [
new NotNull()
]
]
);
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Lead::class,
]);
}
View:
{% extends '#OroAction/Operation/form.html.twig' %}
{% set formAction = path('pb_lead_datagrid_mass_action_change_owner', {gridName: 'sales-lead-grid', actionName: 'change_owner', inset: inset, values: values }) %}
{% block form %}
<form id="{{ form.vars.id }}"
name="{{ form.vars.name }}"
action="{{ formAction }}"
method="{{ form.vars.method }}"
class="form-dialog"
>
<fieldset class="form-horizontal">
{{ form_row(form) }}
</fieldset>
<div class="hidden">
{{ form_rest(form) }}
</div>
<div class="widget-actions">
<button class="btn" type="reset">{{ 'Cancel'|trans }}</button>
<button class="btn btn-success" type="submit">{{ 'Apply'|trans }}</button>
</div>
</form>
{{ oro_form_js_validation(form) }}
{% endblock %}
The question is: Why is the modal not showing?
Thank you very much for the help

the error is that the route must start with "oro_", only that a small detail does not allow to show modal.
change_owner:
type: lead_change_owner_mass_edit
handler: pb_lead.mass_action.change_owner.handler
acl_resource: oro_sales_lead_update
route: oro_lead_mass_action_change_owner
label: pb.lead.mass_actions.change_owner.label
icon: user
data_identifier: lead.id
frontend_options:
title: "#translator->trans(pb.lead.mass_actions.change_owner.label)"
dialogOptions:
width: 500
modal: true
allowMaximize: false
allowMinimize: false

Related

How to fix Error in email validator Angular

I'm trying to create an email centre using the regex pattern but I'm getting an error in validator in HTML. Installed ngx-chips and angular-editor, imported all the modules and the dependencies
HTML-
<div class="row class-spacing">
<div class="col-2">To</div>
<div class="col-8 set-border">
<tag-input
[ngModel]="toEmails"
[displayBy]="'label'"
[identifyBy]="'label'"
[theme]="'bootstrap'"
name="toEmails"
placeholder="+ To"
[separatorKeyCodes]="[32, 188]"
secondaryPlaceholder="To"
[errorMessages]="errorMessages"
[validators]="validators"
[removable]="true"
>
<tag-input-dropdown
[dynamicUpdate]="false"
[zIndex]="10000"
[displayBy]="'label'"
[identifyBy]="'label'"
[showDropdownIfEmpty]="true"
>
<ng-template let-item="item" let-index="index">
{{ item.label }}
</ng-template>
</tag-input-dropdown>
</tag-input>
</div>
</div>
The TypeScript file-
export class EmailComponent implements OnInit {
public errorMessages = {
pattern: 'Email must be in format abc#abc.com',
};
public validators = (this.checkPattern);
#ViewChild('form', { static: false })
form!: NgForm;
public dataModel: any;
public editorTextCount = 0;
public isSubmitted: boolean = false;
public isFieldEmpty: boolean = false;
public isMaxLengthLimitReached: boolean = false;
public config: AngularEditorConfig = {
editable: true,
spellcheck: true,
height: '200px',
width: 'auto',
enableToolbar: true,
showToolbar: true,
placeholder: 'Enter your email',
fonts: [
{ class: 'arial', name: 'Arial' },
{ class: 'times-new-roman', name: 'Times New Roman' },
{ class: 'calibri', name: 'Calibri' },
],
sanitize: true,
toolbarPosition: 'top',
toolbarHiddenButtons: [
[],
['customClasses', 'insertImage', 'insertVideo', 'toggleEditorMode'],
],
};
body: any;
subject: any;
toEmails: any;
public formDetails = {
toEmails: '',
subject: '',
body: '',
};
public items = ['Pizza', 'Pasta', 'Parmesan'];
constructor() {}
ngOnInit() {}
public sendEmail(): void {
console.log(this.form.value);
}
private checkPattern(control: FormControl) {
const patternRegex = /^[A-Za-z0-9._%+-]+#[a-z0-9.-]+\.[a-z]{2,4}$/;
if (patternRegex.test(control.value)) {
return console.log('Match exists.');
} else {
return { pattern: true };
}
}
}
The error I'm getting in the line-
[validators]="validators"
The error I'm getting-
Type '(control: FormControl) => void | { pattern: boolean; }' is not
assignable to type 'ValidatorFn[]'.
Implement a custom validator function requires:
Only one input argument is expected, which is of type AbstractControl. The validator function can obtain the value to be validated via the control.value property
The validator function needs to return null if no errors were found in the field value, meaning that the value is valid
If any validation errors are found, the function needs to return an object of type ValidationErrors
Reference: Angular Custom Form Validators: Complete Guide (How to write a Validator function section)
private checkPattern(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const patternRegex = /^[A-Za-z0-9._%+-]+#[a-z0-9.-]+\.[a-z]{2,4}$/;
if (patternRegex.test(control.value)) {
console.log('Match exists.');
return null;
} else {
return { pattern: true };
}
};
}
Assign validators with a ValidatorFn array containing checkPattern ValidatorFn.
public validators = [this.checkPattern];

Translations reflect after click on combo, where combo options are filled dynamically in ngOnInit in Angular 6

I have an autocomplete drop-down to which I bind list of ViewOption (my class)
class ViewOption {
name: string;
ordinal: number;
label: string
}
I create list of viewoption in ngOnInit by calling getViewOptionList,
getViewOptionList(): ViewOptions {
const viewoptions = new ViewOptions();
for (const enumMember in ViewOptionEnum) {
if (ViewOptionEnum.hasOwnProperty(enumMember)) {
const viewoption = new ViewOption();
const ordinalValue = parseInt(enumMember, 10);
if (ordinalValue >= 0) {
viewoption.oridinal = ordinalValue;
viewoption.label = ViewOptionEnum[enumMember].toLowerCase();
this.translate.get(viewoption.label).subscribe(msg => viewoption.name = msg);
viewoptions.push(viewoption);
}
}
}
return viewoptions;
}
<lis-auto-complete-dropdown placeholder="select view" automationid="worklistMaster_ddl_selectView"
[options]="ViewOptions" [(selectedItem)]="selectedViewOption"
(itemSelected)="selectViewOption($event)">
<ng-template let-item="item">
<span title="{{ item }}">
<span>{{ item.name }}</span>
</span>
</ng-template>
</lis-auto-complete-dropdown>
Translation reflect only when user clicks on autocomple dropdown on UI. How to make it fill before user taking any action?

Delete option for user if he is the owner of the perticular property

I want to show delete option for a user if he has created perticular exhibit. I am getting a current user id from getCurrentUser service and i am getting an array of exhibits in which thre is a field "userId".
I am trying to match id of the current user and userId from Exhibits array in such a way that if there is a match, then only user will get delete option for perticular exhibit but I am unable to do it in proper way.
Below is my code:
-------------------------------------------------------------------------------
ngOnInit() {
this.getCurrentUser();
this.getIsSupervisor();
this.spinnerService.show();
let allRoutesOption = Route.emptyRoute();
allRoutesOption.title = 'ALL';
this.routes = [allRoutesOption];
this.getAllExhibits();
this.routeService.getAllRoutes(1, 100)
.then(
data => this.routes = this.routes.concat(data.items)
).catch(
error => console.error(error)
);
this.getPage(1);
}
ngOnDestroy() {
this.spinnerService.hide();
}
getIsSupervisor() {
this.supervisorGuard.isSupervisor().then(
(response: boolean) => {
this.isSupervisor = response;
});
}
getCurrentUser() {
this.userService.getCurrent()
.then(
(response) => {
this.currentUserId = response.id;
this.exhibitService.getAllExhibits(1, this.maxNumberOfMarkers)
.then(
(data) => {
this.allExhibits = data.items;
for (let exhibit of this.allExhibits) {
this.exhibitsUserIds.push(exhibit.userId);
if (this.exhibitsUserIds !== this.currentUserId) {
this.canDelete = false;
} else {
this.canDelete = true;
}
}
}
);
}
);
}
-------------------------------------------------------------------------------
My Html:
----------------------------------------
<md-nav-list>
<md-list-item [routerLink]="['/mobile-content/exhibits/view', exhibit.id]" ng-blur="true" *ngFor="let exhibit of exhibits | paginate: { id: 'server',
itemsPerPage: exhibitsPerPage,
currentPage: currentPage,
totalItems: totalItems }">
<img md-list-avatar *ngIf="previewsLoaded && previews.has(exhibit.id); else exhibitIcon" [src]="previews.get(exhibit.id)"
alt="{{ 'image preview' | translate }}" [ngStyle]="{'width.px': 48, 'height.px': 48}">
<ng-template #exhibitIcon>
<md-icon md-list-icon class="type-icon" [ngStyle]="{'font-size.px': 40, 'height.px': 40, 'width.px': 40}">place</md-icon>
</ng-template>
<h2 md-line>{{ exhibit.name }} ({{ exhibit.status | translate }})
<hip-star-rating class="fix-position" *ngIf="exhibit.ratings" [rating]='exhibit.ratings' [exhibitId]='exhibit.id'></hip-star-rating>
</h2>
<p md-line>{{ exhibit.description }}</p>
<p md-line>
<span class="latitude">{{ exhibit.latitude }}</span>,
<span class="longitude">{{ exhibit.longitude }}</span>
</p>
<p *ngIf="exhibit.tags.length > 0" md-line>
<span *ngFor="let tag of exhibit.tags" class="tag-name">{{ tag }}</span>
</p>
<button md-icon-button click-stop-propagation color="primary" [routerLink]="['/mobile-content/exhibits/edit', exhibit.id]"
title="{{ 'edit' | translate }}">
<md-icon>{{ !inDeletedPage ? 'edit' : 'remove_red_eye'}}</md-icon>
</button>
<div *ngIf="canDelete">
<button md-icon-button click-stop-propagation color="warn" (click)="deleteExhibit(exhibit)" *ngIf="!exhibit.used && !inDeletedPage"
title="{{ 'delete' | translate }}">
<md-icon>delete_forever</md-icon>
</button>
</div>
</md-list-item>
----------------------------------------
Can someone help me to figure it out?
I am new to Angular myself but maybe I can still help. I am noticing a couple things that may be causing issues.
Firstly
When you are iterating across this.allExhibits, I noticed that the this.canDelete is just one value that you keep reassigning after each iteration. By the end it only represents the 'deleteability' of only the last exhibit.
Perhaps you can create some sort of object or array to map against the for..of iteration of this.allExhibits. That way you can store each resolved value of this.canDelete without overwriting it on each iteration.
example.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'app-example',
templateUrl: './example.component.html'
})
export class ExampleComponent {
currentUser:object = {
name: 'User A',
id: 'A'
};
exhibits:object[] = [
{
title: 'Exhibit A',
id: 'A'
},
{
title: 'Exhibit B',
id: 'B'
},
{
title: 'Exhibit C',
id: 'C'
}
];
constructor() { }
deleteExhibit(index) {
this.exhibits = this.exhibits.filter((_, i) => i != index);
}
}
example.component.html
<div *ngFor="let exhibit of exhibits; let i=index">
<h3>{{exhibit.title}}</h3>
<button *ngIf="exhibit.id == currUser.id" (click)="deleteExhibit(i)">DELETE</button>
<hr/>
</div>
Secondly
I presume the getCurrentUser() is something that happens as the component instantiates. In that case, the *ngIf must await the resolved value of this.canDelete before it can either display or hide the delete button.
Since getCurrentUser() appears to resolve sometime after the component's initial rendering of the view, it maybe be possible that setting the value of this.canDelete is not triggering Angular's change detection.
Perhaps try ChangeDetectorRef.detectChanges() after you resolve the final value of this.canDelete. ChangeDetectorRef is importable from #angular/core and instantiable in the component's constructor: constructor(private changeDetectorRef:ChangeDetectorRef) {}.
Hopefully this helps!

Binding ngModel to complex data

For some time I have been researching if, and how to bind complex model to ngModel. There are articles showing how it can be done for simple data (e.g. string) such as this. But what I want to do is more complex. Let's say that I have a class:
export class MyCoordinates {
longitude: number;
latitude: number;
}
Now I am going to use it in multiple places around the application, so I want to encapsulate it into a component:
<coordinates-form></coordinates-form>
I would also like to pass this model to the component using ngModel to take advantage of things like angular forms but was unsuccessful thus far. Here is an example:
<form #myForm="ngForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">Name</label>
<input type="text" [(ngModel)]="model.name" name="name">
</div>
<div class="form-group">
<label for="coordinates">Coordinates</label>
<coordinates-form [(ngModel)]="model.coordinates" name="coordinates"></coordinates-form>
</div>
<button type="submit" class="btn btn-success">Submit</button>
</form>
Is actually possible to do it this way or is my approach simply wrong? For now I have settled on using component with normal input and emitting event on change but I feel like it will get messy pretty fast.
import {
Component,
Optional,
Inject,
Input,
ViewChild,
} from '#angular/core';
import {
NgModel,
NG_VALUE_ACCESSOR,
} from '#angular/forms';
import { ValueAccessorBase } from '../Base/value-accessor';
import { MyCoordinates } from "app/Models/Coordinates";
#Component({
selector: 'coordinates-form',
template: `
<div>
<label>longitude</label>
<input
type="number"
[(ngModel)]="value.longitude"
/>
<label>latitude</label>
<input
type="number"
[(ngModel)]="value.latitude"
/>
</div>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CoordinatesFormComponent,
multi: true,
}],
})
export class CoordinatesFormComponent extends ValueAccessorBase<MyCoordinates> {
#ViewChild(NgModel) model: NgModel;
constructor() {
super();
}
}
ValueAccessorBase:
import {ControlValueAccessor} from '#angular/forms';
export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
private innerValue: T;
private changed = new Array<(value: T) => void>();
private touched = new Array<() => void>();
get value(): T {
return this.innerValue;
}
set value(value: T) {
if (this.innerValue !== value) {
this.innerValue = value;
this.changed.forEach(f => f(value));
}
}
writeValue(value: T) {
this.innerValue = value;
}
registerOnChange(fn: (value: T) => void) {
this.changed.push(fn);
}
registerOnTouched(fn: () => void) {
this.touched.push(fn);
}
touch() {
this.touched.forEach(f => f());
}
}
Usage:
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)">
<coordinates-form
required
hexadecimal
name="coordinatesModel"
[(ngModel)]="coordinatesModel">
</coordinates-form>
<button type="Submit">Submit</button>
</form>
The error I am getting Cannot read property 'longitude' of undefined. For simple model, like string or number it works without a problem.
The value property is undefined at first.
To solve this issue you need to change your binding like:
[ngModel]="value?.longitude" (ngModelChange)="value.longitude = $event"
and change it for latitude as well
[ngModel]="value?.latitude" (ngModelChange)="value.latitude = $event"
Update
Just noticed you're running onChange event within settor so you need to change reference:
[ngModel]="value?.longitude" (ngModelChange)="handleInput('longitude', $event)"
[ngModel]="value?.latitude" (ngModelChange)="handleInput('latitude', $event)"
handleInput(prop, value) {
this.value[prop] = value;
this.value = { ...this.value };
}
Updated Plunker
Plunker Example with google map
Update 2
When you deal with custom form control you need to implement this interface:
export interface ControlValueAccessor {
/**
* Write a new value to the element.
*/
writeValue(obj: any): void;
/**
* Set the function to be called when the control receives a change event.
*/
registerOnChange(fn: any): void;
/**
* Set the function to be called when the control receives a touch event.
*/
registerOnTouched(fn: any): void;
/**
* This function is called when the control status changes to or from "DISABLED".
* Depending on the value, it will enable or disable the appropriate DOM element.
*
* #param isDisabled
*/
setDisabledState?(isDisabled: boolean): void;
}
Here is a minimal implementation:
export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
// view => control
onChange = (value: T) => {};
onTouched = () => {};
writeValue(value: T) {
// control -> view
}
registerOnChange(fn: (_: any) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
}
It will work for any type of value. Just implement it for your case.
You do not need here array like (see update Plunker)
private changed = new Array<(value: T) => void>();
When component gets new value it will run writeValue where you need to update some value that will be used in your custom template. In your example you are updating value property which is used together with ngModel in template.
In my example i am drawing new marker.
DefaultValueAccessor just updates value property https://github.com/angular/angular/blob/4.2.0-rc.0/packages/forms/src/directives/default_value_accessor.ts#L76
Datepicker in angular2 material is setting inner value as you do https://github.com/angular/material2/blob/123d7eced4b4f808fc03c945504d68280752d533/src/lib/datepicker/datepicker-input.ts#L202
When you need to propagate changes to AbstractControl you have to call onChange method which you registered in registerOnChange.
I wrote this.value = { ...this.value }; because it is just
this.value = Object.assign({}, this.value)
it will call setter where you call onChange method
Another way is calling onChange directly that is usually used
this.onChange(this.value);
Your example https://plnkr.co/edit/Q11HXhWKrndrA8Tjr6KH?p=preview
DefaultValueAccessor https://github.com/angular/angular/blob/4.2.0-rc.0/packages/forms/src/directives/default_value_accessor.ts#L88
Material2 https://github.com/angular/material2/blob/123d7eced4b4f808fc03c945504d68280752d533/src/lib/datepicker/datepicker-input.ts#L173
You can do anything you like inside custom component. It can have any template and any nested components. But you have to implement logic for ControlValueAccessor to do it working with angular form.
If you open some library such angular2 material or primeng you can find a lot of example how to implement such controls

How to limit max char on redactor limiter plugin?

I have this code in my view
<div class="col-xs-6">
<?php echo $form->field($model, 'deskripsi_produk')->widget(Redactor::className(),['clientOptions'=>['autoresize'=>'true', 'limiter' => 20, 'plugins' => ['limiter'], 'buttons'=> ['html', 'formatting', 'bold', 'italic','underline','lists','horizontalrule'],]]);?>
</div>
it can limit 20 char but How I can limit text area to max string defined on model instead of limit with specific number?
Here is my model
class TbProduk extends \yii\db\ActiveRecord
{
/**
* #inheritdoc
*/
public $image;
public static function tableName()
{
return 'tb_produk';
}
public function rules()
{
return [
...
[['deskripsi_produk'], 'string', 'max' => 2000],
[['deskripsi_produk'], 'checkDesc'],
...
];
}
}
Trying to get the value via rules is a terrible idea. Instead you should create a constant in TbProduk that will hold the length:
class TbProduk extends \yii\db\ActiveRecord
{
const DESKRIPSI_PRODUK_LENGTH = 2000;
...
public function rules()
{
return [
...
[['deskripsi_produk'], 'string', 'max' => static::DESKRIPSI_PRODUK_LENGTH],
...
];
}
}
And in your view:
<div class="col-xs-6">
<?php echo $form->field($model, 'deskripsi_produk')->widget(Redactor::className(),['clientOptions'=>['autoresize'=>'true', 'limiter' => TbProduk::DESKRIPSI_PRODUK_LENGTH, 'plugins' => ['limiter'], 'buttons'=> ['html', 'formatting', 'bold', 'italic','underline','lists','horizontalrule'],]]);?>
</div>