From 4040fff780ed4924052ddd50f9d37a41dedc347e Mon Sep 17 00:00:00 2001 From: Artemiy Solopov Date: Tue, 14 Apr 2026 01:26:28 +0300 Subject: [PATCH] Workflow status creation (extra forms) --- .../stylesheets/project_admin/workflows.css | 15 ++++ .../workflows/statuses_controller.rb | 2 +- .../controllers/dynamic_forms_controller.js | 74 +++++++++++++++++++ .../workflows/statuses/batch_update.rb | 36 ++++++--- .../statuses/_batch_update_fieldset.html.slim | 17 +++++ .../workflows/statuses/edit.html.slim | 26 +++---- 6 files changed, 146 insertions(+), 24 deletions(-) create mode 100644 app/assets/stylesheets/project_admin/workflows.css create mode 100644 app/javascript/controllers/dynamic_forms_controller.js create mode 100644 app/views/project_admin/workflows/statuses/_batch_update_fieldset.html.slim diff --git a/app/assets/stylesheets/project_admin/workflows.css b/app/assets/stylesheets/project_admin/workflows.css new file mode 100644 index 0000000..9d94468 --- /dev/null +++ b/app/assets/stylesheets/project_admin/workflows.css @@ -0,0 +1,15 @@ +.workflow-statuses-batch-update-fieldsets { + display: grid; + grid-template-columns: repeat(3, auto) min-content; + gap: 0.5em; + + > .fieldset { + display: grid; + grid-column: 1 / -1; + grid-template-columns: subgrid; + + .buttons { + align-items: flex-end; + } + } +} diff --git a/app/controllers/project_admin/workflows/statuses_controller.rb b/app/controllers/project_admin/workflows/statuses_controller.rb index c329d92..e8e3543 100644 --- a/app/controllers/project_admin/workflows/statuses_controller.rb +++ b/app/controllers/project_admin/workflows/statuses_controller.rb @@ -17,7 +17,7 @@ module ProjectAdmin end @form = ProjectAdmin::Workflows::Statuses::BatchUpdate.new(form_params) - if @form.perform(@workflow) + if @form.call(@workflow) redirect_to project_admin_workflow_path(@project, @workflow) else render :edit diff --git a/app/javascript/controllers/dynamic_forms_controller.js b/app/javascript/controllers/dynamic_forms_controller.js new file mode 100644 index 0000000..10be527 --- /dev/null +++ b/app/javascript/controllers/dynamic_forms_controller.js @@ -0,0 +1,74 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller { + static targets = ['template', 'container', 'subform'] + static values = { + fieldName: String, + fieldTemplate: String, + indexTemplate: String, + count: Number + } + + connect() { + this.extraFieldsCount = 0 + } + + addForm() { + this.countValue++; + this.extraFieldsCount++; + const content = document.importNode(this.templateTarget.content, true) + for (let element of content.querySelectorAll('label, input, select, textarea')) { + this._replaceAttr(element, 'name') + this._replaceAttr(element, 'id') + this._replaceAttr(element, 'for') + } + + let idInput = content.querySelector('input[name$="[id]"]') + if (idInput) { + idInput.value = `_${this.extraFieldsCount}` + } + + this._subformsContainer.insertAdjacentElement('beforeend', content.children[0]) + } + + toggleSubform(event) { + // TODO: maybe extract into a separate controller + let target = event.currentTarget + let subform = this._findSubform(target) + let disabled = subform.toggleAttribute('data-disabled') + for (let input of subform.querySelectorAll('input:not([type="hidden"]), select, textarea')) { + input.toggleAttribute('disabled', disabled) + } + let destroyInput = subform.querySelector('input[name$="[_destroy]"]') + if(destroyInput) { destroyInput.value = disabled ? 'true' : '' } + if(target.tagName == 'BUTTON') { + target.setAttribute('aria-pressed', disabled ? 'true' : 'false') + } + } + + deleteSubform(event) { + let subform = this._findSubform(event.currentTarget) + subform.remove() + } + + get _subformsContainer() { + if(this.hasContainerTarget) { return this.containerTarget } + + return this.element + } + + _findSubform(element) { + for (let subform of this.subformTargets) { + if(subform.contains(element)) { return subform } + } + } + + _replaceAttr(element, attr) { + let value = element.getAttribute(attr) + if(value == null) return; + + value = value.replace(this.fieldTemplateValue, this.fieldNameValue).replace(this.indexTemplateValue, this.countValue) + element.setAttribute(attr, value) + } + +} diff --git a/app/services/project_admin/workflows/statuses/batch_update.rb b/app/services/project_admin/workflows/statuses/batch_update.rb index c183228..783745b 100644 --- a/app/services/project_admin/workflows/statuses/batch_update.rb +++ b/app/services/project_admin/workflows/statuses/batch_update.rb @@ -28,27 +28,45 @@ module ProjectAdmin @task_statuses = Array(attributes).map { |e| TaskStatus.new(e) } end - def perform(workflow) + def call(workflow) @workflow = workflow task_status_models = @workflow.task_statuses.index_by(&:id) @workflow.transaction(requires_new: true) do task_statuses.each do |ts| - model = task_status_models.fetch(ts.id.to_i) - if ts._destroy - model.destroy! + if ts.id.start_with?('_') + create_model!(ts) else - model.update!( - name: ts.name, - icon: ts.icon, - color: ts.color - ) + model = task_status_models.fetch(Integer(ts.id)) + if ts._destroy + model.destroy! + else + update_model!(model, ts) + end end end end true end + + private + + def update_model!(model, form) + model.update!( + name: form.name, + icon: form.icon, + color: form.color + ) + end + + def create_model!(form) + @workflow.task_statuses.create!( + name: form.name, + icon: form.icon, + color: form.color + ) + end end end end diff --git a/app/views/project_admin/workflows/statuses/_batch_update_fieldset.html.slim b/app/views/project_admin/workflows/statuses/_batch_update_fieldset.html.slim new file mode 100644 index 0000000..3741449 --- /dev/null +++ b/app/views/project_admin/workflows/statuses/_batch_update_fieldset.html.slim @@ -0,0 +1,17 @@ +- with_destroy ||= false + +div.fieldset data-dynamic-forms-target="subform" + = ff.hidden_field :id + - if with_destroy + = ff.hidden_field :_destroy + .field + = ff.label :name + = ff.text_field :name + .field + = ff.label :color + = ff.select :color, TaskStatus.colors + .field + = ff.label :icon + = ff.select :icon, TaskStatus.icons + .buttons + = button_tag 'Destroy', type: :button, class: 'danger', data: { action: (with_destroy ? 'dynamic-forms#toggleSubform' : 'dynamic-forms#deleteSubform') } diff --git a/app/views/project_admin/workflows/statuses/edit.html.slim b/app/views/project_admin/workflows/statuses/edit.html.slim index 1bcfdfe..c61ddee 100644 --- a/app/views/project_admin/workflows/statuses/edit.html.slim +++ b/app/views/project_admin/workflows/statuses/edit.html.slim @@ -1,16 +1,14 @@ -= form_with model: @form, scope: 'workflow', url: project_admin_workflow_statuses_path(@project, @workflow), method: :put do |f| - = f.fields_for :task_statuses, include_id: false do |tsf| - fieldset - = tsf.hidden_field :id - = tsf.hidden_field :_destroy - .field - = tsf.label :name - = tsf.text_field :name - .field - = tsf.label :color - = tsf.select :color, TaskStatus.colors - .field - = tsf.label :icon - = tsf.select :icon, TaskStatus.icons += form_with model: @form, scope: 'workflow', url: project_admin_workflow_statuses_path(@project, @workflow), method: :put, data: {controller: 'dynamic-forms', 'dynamic-forms-field-name-value': 'task_statuses_attributes', 'dynamic-forms-field-template-value': '_extra_form', 'dynamic-forms-index-template-value': '__index__', 'dynamic-forms-count-value': @form.task_statuses.count} do |f| + section.workflow-statuses-batch-update-fieldsets data-dynamic-forms-target="container" + = f.fields_for :task_statuses, include_id: false do |tsf| + = render 'batch_update_fieldset', ff: tsf, with_destroy: true + + .buttons + button.success type="button" data-action="dynamic-forms#addForm" Add status + + = f.fields_for :'_extra_form', index: '__index__' do |exf| + template data-dynamic-forms-target="template" + = render 'batch_update_fieldset', ff: exf + .submit = f.submit