[37026] Add new project form with template selection (#9193)
* Projects form working with formly 50% * Removed console.log * Working with formattable * Working with formattable * Input with id and label * Input with id and label * Useless dependencies removed * Saving forms + required labels with * * First backend validation approach * Removed reload on type change + keep model on route changes * Handlig backend validations with setError * Formatting the form model to submit * Make up refactor * working with op-form-field * Form creation moved to the service * Working with op-form-field wrapper * Working with validation and op-form-field * Working with []CustomFields * Clean up * Clean up * Clean up * Clean up * Form routing working * Notification on form error and success * Refactor + removed useless dynamic form observable * DynamicFieldsService with tests * Refactor: inputs catalog + catch form load error * Filter out non writable fields * Refactor: naming consistency * Cleaning comments * dynamic-fields-service tests + wrapper component * DynamicForm Tests * @ngx-formly/core dependency added * Cleaning up * Provide DynamicFieldsService in root so it can be used independently * DynamicForm working as a FormControl * Getting route params sync * Global FormsService: submit + formatting + error handling * Fix: @Optional() FormGroupDirective in OpFormFieldComponent * Code climate fix * Removed CdkTextareaAutosize because of CDK issue 22469 * DynamicFormComponent tests * Dynamic input test helpers + boolean and text tests * Refactor edit fields to avoid circular dependencies in the dynamic forms * Naming fix * IntegerInputComponent tests * SelectInputComponent tests * Fix: duplicated identifier on inputs * Extract toolbar to be reused for now Still TBD whether we want to move them right now to the frontend? * Create new project route and redirect to rails view after saving * fieldsSettingsPipe + hide 'identifier' on projects * Handling multi-values (also as links) and passwords * Some TODOs removed * FormattableTextareaInputComponent tests * Projects form working with formly 50% * Removed console.log * Working with formattable * Working with formattable * Input with id and label * Input with id and label * Useless dependencies removed * Saving forms + required labels with * * First backend validation approach * Removed reload on type change + keep model on route changes * Handlig backend validations with setError * Formatting the form model to submit * Make up refactor * working with op-form-field * Form creation moved to the service * Working with op-form-field wrapper * Working with validation and op-form-field * Working with []CustomFields * Clean up * Clean up * Clean up * Clean up * Form routing working * Notification on form error and success * Refactor + removed useless dynamic form observable * DynamicFieldsService with tests * Refactor: inputs catalog + catch form load error * Filter out non writable fields * Refactor: naming consistency * Cleaning comments * dynamic-fields-service tests + wrapper component * DynamicForm Tests * @ngx-formly/core dependency added * Cleaning up * DynamicForm working as a FormControl * Getting route params sync * Global FormsService: submit + formatting + error handling * Fix: @Optional() FormGroupDirective in OpFormFieldComponent * Code climate fix * Removed CdkTextareaAutosize because of CDK issue 22469 * DynamicFormComponent tests * Dynamic input test helpers + boolean and text tests * Refactor edit fields to avoid circular dependencies in the dynamic forms * Naming fix * IntegerInputComponent tests * SelectInputComponent tests * Fix: duplicated identifier on inputs * Extract toolbar to be reused for now Still TBD whether we want to move them right now to the frontend? * Create new project route and redirect to rails view after saving * fieldsSettingsPipe + hide 'identifier' on projects * Handling multi-values (also as links) and passwords * Some TODOs removed * FormattableTextareaInputComponent tests * _isResourceSchema based on parent?.location * Scope DynamicFieldsService to DynamicFormComponent * Added backend validation method to FormsService * Remove form from DynamicForm when not isStandaloneForm * Allow multiple form keys to validate * Remove form from non standalone forms * Remove duplicated button * Fix: dynamic form with ng-template not showing validations * DynamicForm programatic validation * Add new project component that posts to the copy form * Hide the submit button input * Unify ckeditor spacings with other dynamic-form elements * Basic fixes to invalid dynamic form fields * Load the templated projects with newly added action * Fix invalid styles for dynamic inputs * Updates to form styles, add op-form and op-fieldset * Soften inputs to 2px * Update input styles globally * Redirect to job status modal when response is job status * fix: load projects routes * Collapsible groups working * Fix: setTimeout datepicker to wait DOM ready * resourceId added to project settings form * fieldsLayoutConfig * formUrl & formHttpMethod @Inputs * Avoid replacing non ending '/form' strings * formUrl @Input * @Input model * identifier hidden * Populate the model * Add new project component * Tests removed * Show create message when creating new resource * Change location to rails * Add field_name helper for testing * Add form field helper classes and fix tests * Remove templated from copy for now * Fix more tests * Reset toolbar from dev * Skip identifier test * Set overflow to visible when expanded * Fix more tests * Rename data-field-name to data-qa-field-name * Review feedback * Uncollapse group fields when they contains errors * Fix text spec * Remove instantiate template service * Remove unnecessary ProjectController#create Co-authored-by: Aleix Suau <info@macrofonoestudio.es> Co-authored-by: Benjamin Bädorf <b.baedorf@openproject.com>pull/9217/head
parent
f309cb9b09
commit
71b330ea80
@ -1,55 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Projects |
||||
class InstantiateTemplateContract < CreateContract |
||||
def self.visible_templates(user) |
||||
Project |
||||
.allowed_to(user, :copy_projects) |
||||
.where(templated: true) |
||||
end |
||||
|
||||
validate :validate_user_allowed_to_instantiate_template |
||||
|
||||
private |
||||
|
||||
def validate_user_allowed_to_instantiate_template |
||||
errors.add(:base, :error_unauthorized) unless visible_template? |
||||
end |
||||
|
||||
def visible_template? |
||||
return false if template_project_id.nil? |
||||
|
||||
self.class.visible_templates(user).exists?(template_project_id) |
||||
end |
||||
|
||||
def template_project_id |
||||
options[:template_project_id] |
||||
end |
||||
end |
||||
end |
@ -1,74 +0,0 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Projects |
||||
class InstantiateTemplateService < ::BaseServices::Create |
||||
attr_reader :template_id |
||||
|
||||
def initialize(user:, template_id:) |
||||
@template_id = template_id |
||||
|
||||
super user: user, |
||||
contract_class: Projects::InstantiateTemplateContract, |
||||
contract_options: { template_project_id: template_id } |
||||
end |
||||
|
||||
def after_validate(params, _) |
||||
job = ::CopyProjectJob.perform_later( |
||||
user_id: user.id, |
||||
source_project_id: template_id, |
||||
target_project_params: project_params(params), |
||||
# Copy all associations |
||||
associations_to_copy: nil, |
||||
# Send mails for now until we send our own mails |
||||
send_mails: true |
||||
) |
||||
|
||||
ServiceResult.new(success: true, result: job) |
||||
end |
||||
|
||||
# Do not actually try to save the project here |
||||
# but simply pass the previous call |
||||
def persist(call) |
||||
call |
||||
end |
||||
|
||||
private |
||||
|
||||
## |
||||
# Modifies params to ensure we unset |
||||
# the templated option |
||||
def project_params(params) |
||||
params.to_h.merge( |
||||
templated: false |
||||
) |
||||
end |
||||
end |
||||
end |
@ -1,7 +1,30 @@ |
||||
<div class="op-field-group-wrapper"> |
||||
<div class="op-field-group-wrapper--header">{{ to.label }}</div> |
||||
<fieldset class="form--fieldset -collapsible" |
||||
[ngClass]="{'collapsed': to.collapsibleFieldGroupsCollapsed}" |
||||
*ngIf="to?.collapsibleFieldGroups"> |
||||
<legend class="form--fieldset-legend" |
||||
title="Show/hide" |
||||
(click)="to.collapsibleFieldGroupsCollapsed = !to.collapsibleFieldGroupsCollapsed"> |
||||
<a href="#" class="op-form-group--label"> |
||||
{{ to.label }} |
||||
</a> |
||||
</legend> |
||||
|
||||
<div class="op-field-group-wrapper--body"> |
||||
<ng-container #fieldComponent></ng-container> |
||||
</div> |
||||
</div> |
||||
<div class="op-form-group--content" |
||||
[ngStyle]="{ |
||||
'height': to.collapsibleFieldGroupsCollapsed ? 0 : 'auto', |
||||
'visibility': to.collapsibleFieldGroupsCollapsed ? 'hidden' : 'visible', |
||||
'overflow': to.collapsibleFieldGroupsCollapsed ? 'hidden' : 'visible' |
||||
}"> |
||||
<ng-container #fieldComponent></ng-container> |
||||
</div> |
||||
</fieldset> |
||||
|
||||
<ng-container *ngIf="!to?.collapsibleFieldGroups"> |
||||
<div class="op-form-group--label"> |
||||
{{ to.label }} |
||||
</div> |
||||
|
||||
<div class="op-form-group--content"> |
||||
<ng-container #fieldComponent></ng-container> |
||||
</div> |
||||
</ng-container> |
@ -0,0 +1,11 @@ |
||||
<op-form-field [label]="to?.label" |
||||
[noWrapLabel]="to?.noWrapLabel" |
||||
[required]="to?.required" |
||||
[attr.data-qa-field-name]="to?.property"> |
||||
<ng-container #fieldComponent slot=input></ng-container> |
||||
|
||||
<formly-validation-message class="op-form-field--error" |
||||
[field]="field" |
||||
slot="errors"> |
||||
</formly-validation-message> |
||||
</op-form-field> |
@ -0,0 +1,11 @@ |
||||
import { Component, ChangeDetectionStrategy } from '@angular/core'; |
||||
import { FieldWrapper } from "@ngx-formly/core"; |
||||
|
||||
@Component({ |
||||
selector: 'op-dynamic-field-wrapper', |
||||
templateUrl: './dynamic-field-wrapper.component.html', |
||||
styleUrls: ['./dynamic-field-wrapper.component.sass'], |
||||
changeDetection: ChangeDetectionStrategy.OnPush |
||||
}) |
||||
export class DynamicFieldWrapperComponent extends FieldWrapper { |
||||
} |
@ -0,0 +1,27 @@ |
||||
<form |
||||
class="op-form" |
||||
[formGroup]="templateForm" |
||||
> |
||||
<op-form-field |
||||
[label]="text.use_template" |
||||
data-qa-field-name="use_template" |
||||
> |
||||
<ng-select |
||||
[formControl]="templateControl" |
||||
[items]="templateOptions$ | async" |
||||
(ngModelChange)="onTemplateSelected($event)" |
||||
bindLabel="name" |
||||
slot="input" |
||||
> |
||||
</ng-select> |
||||
</op-form-field> |
||||
</form> |
||||
|
||||
<op-dynamic-form |
||||
[formUrl]="formUrl" |
||||
[resourcePath]="resourcePath" |
||||
[initialPayload]="initialPayload" |
||||
[fieldsSettingsPipe]="dynamicFieldsSettingsPipe" |
||||
(submitted)="onSubmitted($event)" |
||||
> |
||||
</op-dynamic-form> |
@ -0,0 +1,181 @@ |
||||
import {Component, OnInit} from '@angular/core'; |
||||
import {StateService, UIRouterGlobals} from "@uirouter/core"; |
||||
import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin"; |
||||
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; |
||||
import {HalResource, HalSource} from "core-app/modules/hal/resources/hal-resource"; |
||||
import {IOPFormlyFieldSettings} from "core-app/modules/common/dynamic-forms/typings"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {FormControl, FormGroup} from "@angular/forms"; |
||||
import {APIV3Service} from "core-app/modules/apiv3/api-v3.service"; |
||||
import {ApiV3FilterBuilder} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
import {map} from "rxjs/operators"; |
||||
import {Observable} from "rxjs"; |
||||
import {JobStatusModal} from "core-app/modules/job-status/job-status-modal/job-status.modal"; |
||||
import {OpModalService} from "core-app/modules/modal/modal.service"; |
||||
import { FormlyFieldConfig } from "@ngx-formly/core"; |
||||
|
||||
export interface ProjectTemplateOption { |
||||
href:string|null; |
||||
title:string; |
||||
} |
||||
|
||||
@Component({ |
||||
selector: 'op-new-project', |
||||
templateUrl: './new-project.component.html', |
||||
}) |
||||
export class NewProjectComponent extends UntilDestroyedMixin implements OnInit { |
||||
resourcePath:string; |
||||
dynamicFieldsSettingsPipe = this.fieldSettingsPipe.bind(this); |
||||
|
||||
initialPayload = {}; |
||||
|
||||
formUrl:string; |
||||
text = { |
||||
use_template: this.I18n.t('js.project.use_template'), |
||||
}; |
||||
|
||||
hiddenFields:string[] = [ |
||||
'identifier', |
||||
'sendNotifications', |
||||
'active' |
||||
]; |
||||
|
||||
copyableTemplateFilter = new ApiV3FilterBuilder() |
||||
.add('user_action', '=', ["projects/copy"]) // no null values
|
||||
.add('templated', '=', true); |
||||
|
||||
templateOptions$:Observable<ProjectTemplateOption[]> = |
||||
this |
||||
.apiV3Service |
||||
.projects |
||||
.filtered(this.copyableTemplateFilter) |
||||
.get() |
||||
.pipe( |
||||
map(response => |
||||
response.elements.map((el:HalResource) => ({ href: el.href, name: el.name }))), |
||||
); |
||||
|
||||
templateForm = new FormGroup({ |
||||
template: new FormControl(), |
||||
}); |
||||
|
||||
get templateControl() { |
||||
return this.templateForm.get('template'); |
||||
} |
||||
|
||||
constructor( |
||||
private apiV3Service:APIV3Service, |
||||
private uIRouterGlobals:UIRouterGlobals, |
||||
private pathHelperService:PathHelperService, |
||||
private modalService:OpModalService, |
||||
private $state:StateService, |
||||
private I18n:I18nService, |
||||
) { |
||||
super(); |
||||
} |
||||
|
||||
ngOnInit():void { |
||||
this.resourcePath = this.pathHelperService.projectsPath(); |
||||
|
||||
if (this.uIRouterGlobals.params.parent_id) { |
||||
this.setParentAsPayload(this.uIRouterGlobals.params.parent_id); |
||||
} |
||||
} |
||||
|
||||
onSubmitted(response:HalSource) { |
||||
if (response._type === 'JobStatus') { |
||||
this.modalService.show(JobStatusModal, 'global', { jobId: response.jobId }); |
||||
} else { |
||||
window.location.href = this.pathHelperService.projectPath(response.identifier as string); |
||||
} |
||||
} |
||||
|
||||
onTemplateSelected(selected:{ href:string|null }) { |
||||
if (selected.href) { |
||||
this.formUrl = selected.href + '/copy'; |
||||
} else { |
||||
this.resourcePath = this.pathHelperService.projectsPath(); |
||||
} |
||||
} |
||||
|
||||
private isHiddenField(key:string|undefined):boolean { |
||||
return !!key && (this.hiddenFields.includes(key) || this.isMeta(key)); |
||||
} |
||||
|
||||
private isMeta(key:string):boolean { |
||||
return key.startsWith('copy'); |
||||
} |
||||
|
||||
private setParentAsPayload(parentId:string) { |
||||
const href = this.apiV3Service.projects.id(parentId).path; |
||||
|
||||
this.initialPayload = { |
||||
_links: { |
||||
parent: { |
||||
href: href |
||||
} |
||||
} |
||||
}; |
||||
} |
||||
|
||||
private fieldSettingsPipe(dynamicFieldsSettings:IOPFormlyFieldSettings[]):IOPFormlyFieldSettings[] { |
||||
const fieldsLayoutConfig = dynamicFieldsSettings |
||||
.reduce((result, field) => { |
||||
field = { |
||||
...field, |
||||
hide: this.isHiddenField(field.key), |
||||
} |
||||
|
||||
if ( |
||||
(field.templateOptions?.required && |
||||
!field.templateOptions.hasDefault && |
||||
field.templateOptions.payloadValue == null) || |
||||
field.templateOptions?.property === 'name' || |
||||
field.templateOptions?.property === 'parent' |
||||
) { |
||||
result.firstLevelFields = [...result.firstLevelFields, field]; |
||||
} else { |
||||
result.advancedSettingsFields = [...result.advancedSettingsFields, field]; |
||||
} |
||||
|
||||
return result; |
||||
}, { |
||||
firstLevelFields: [], |
||||
advancedSettingsFields: [] |
||||
} as { firstLevelFields:IOPFormlyFieldSettings[], advancedSettingsFields:IOPFormlyFieldSettings[] }); |
||||
|
||||
const advancedSettingsGroup = { |
||||
fieldGroup: fieldsLayoutConfig.advancedSettingsFields, |
||||
fieldGroupClassName: "op-form--field-group", |
||||
templateOptions: { |
||||
label: this.I18n.t("js.forms.advanced_settings"), |
||||
isFieldGroup: true, |
||||
collapsibleFieldGroups: true, |
||||
collapsibleFieldGroupsCollapsed: true, |
||||
}, |
||||
type: "formly-group" as "formly-group", |
||||
wrappers: ["op-dynamic-field-group-wrapper"], |
||||
expressionProperties: { |
||||
'templateOptions.collapsibleFieldGroupsCollapsed': (model:unknown, formState:unknown, field:FormlyFieldConfig) => { |
||||
// Uncollapse field groups when the form has errors and has been submitted
|
||||
if ( |
||||
field.type !== 'formly-group' || |
||||
!field.templateOptions?.collapsibleFieldGroups || |
||||
!field.templateOptions?.collapsibleFieldGroupsCollapsed |
||||
) { |
||||
return; |
||||
} else { |
||||
return !( |
||||
field.fieldGroup?.some(groupField => |
||||
groupField?.formControl?.errors && |
||||
!groupField.hide && |
||||
field.options?.parentForm?.submitted |
||||
)); |
||||
} |
||||
}, |
||||
} |
||||
} |
||||
|
||||
return [...fieldsLayoutConfig.firstLevelFields, advancedSettingsGroup]; |
||||
} |
||||
} |
@ -1,5 +1,4 @@ |
||||
<op-dynamic-form [resourcePath]="projectsPath" |
||||
[resourceId]="resourceId" |
||||
[fieldsSettingsPipe]="dynamicFieldsSettingsPipe" |
||||
(submitted)="onSubmitted($event)"> |
||||
<op-dynamic-form [resourceId]="resourceId" |
||||
[resourcePath]="projectsPath" |
||||
[fieldsSettingsPipe]="dynamicFieldsSettingsPipe"> |
||||
</op-dynamic-form> |
@ -1,74 +0,0 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
require 'spec_helper' |
||||
require_relative './shared_contract_examples' |
||||
require 'contracts/shared/model_contract_shared_context' |
||||
|
||||
describe Projects::InstantiateTemplateContract do |
||||
include_context 'ModelContract shared context' |
||||
|
||||
let(:user) { FactoryBot.build_stubbed :user } |
||||
let(:project) { Project.new name: 'Foo Bar', identifier: 'foo' } |
||||
let(:template) { FactoryBot.build_stubbed :project } |
||||
let(:options) { { template_project_id: template.id } } |
||||
|
||||
let(:contract) { described_class.new(project, user, options: options) } |
||||
|
||||
before do |
||||
allow(user) |
||||
.to(receive(:allowed_to_globally?)) |
||||
.with(:add_project) |
||||
.and_return(allowed_to_add) |
||||
|
||||
allow(Project) |
||||
.to receive_message_chain(:allowed_to, :where, :exists?) |
||||
.and_return(allowed_to_copy) |
||||
end |
||||
|
||||
context 'when user may copy template' do |
||||
let(:allowed_to_copy) { true } |
||||
let(:allowed_to_add) { true } |
||||
|
||||
it_behaves_like 'contract is valid' |
||||
|
||||
context 'but may not add projects' do |
||||
let(:allowed_to_copy) { true } |
||||
let(:allowed_to_add) { false } |
||||
|
||||
it_behaves_like 'contract is invalid', base: :error_unauthorized |
||||
end |
||||
end |
||||
|
||||
context 'when user may not copy template' do |
||||
let(:allowed_to_copy) { false } |
||||
let(:allowed_to_add) { true } |
||||
|
||||
it_behaves_like 'contract is invalid', base: :error_unauthorized |
||||
end |
||||
end |
@ -0,0 +1,29 @@ |
||||
module FormFields |
||||
class FormField |
||||
include Capybara::DSL |
||||
include RSpec::Matchers |
||||
|
||||
attr_reader :property, :selector |
||||
|
||||
def initialize(property, selector: nil) |
||||
@property = property |
||||
@selector = selector || "[data-qa-field-name='#{property_name}']" |
||||
end |
||||
|
||||
def expect_visible |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
def field_container |
||||
page.find(selector) |
||||
end |
||||
|
||||
def property_name |
||||
if property.is_a? CustomField |
||||
"customField#{property.id}" |
||||
else |
||||
property.to_s |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,30 @@ |
||||
require_relative './form_field' |
||||
|
||||
module FormFields |
||||
class InputFormField < FormField |
||||
|
||||
def expect_value(value) |
||||
expect(field_container).to have_selector('input') { |el| el.value == value } |
||||
end |
||||
|
||||
def expect_visible |
||||
expect(field_container).to have_selector('input') |
||||
end |
||||
|
||||
## |
||||
# Set or select the given value. |
||||
# For fields of type select, will check for an option with that value. |
||||
def set_value(content) |
||||
scroll_to_element(input_element) |
||||
|
||||
# A normal fill_in would cause the focus loss on the input for empty strings. |
||||
# Thus the form would be submitted. |
||||
# https://github.com/erikras/redux-form/issues/686 |
||||
input_element.fill_in with: content, fill_options: { clear: :backspace } |
||||
end |
||||
|
||||
def input_element |
||||
field_container.find 'input' |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,23 @@ |
||||
require_relative './form_field' |
||||
|
||||
module FormFields |
||||
class SelectFormField < FormField |
||||
def expect_selected(*values) |
||||
values.each do |val| |
||||
expect(field_container).to have_selector('.ng-value', text: val) |
||||
end |
||||
end |
||||
|
||||
def expect_visible |
||||
expect(field_container).to have_selector('ng-select') |
||||
end |
||||
|
||||
def select_option(*values) |
||||
values.each do |val| |
||||
field_container.find('.ng-select-container').click |
||||
page.find('.ng-option', text: val, visible: :all).click |
||||
sleep 1 |
||||
end |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue