Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[57423] Progress modal should always prevent save when there are invalid values #16735

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
id: "progress-form",
html: { autocomplete: "off" },
data: { "application-target": "dynamic",
"work-packages--progress--preview-progress-target": "form",
"work-packages--progress--preview-target": "form",
controller: "work-packages--progress--focus-field " \
"work-packages--progress--preview-progress " \
"work-packages--progress--touched-field-marker" }
"work-packages--progress--preview" }
) do |f| %>
<%= flex_layout do |modal_body| %>
<% modal_body.with_row(classes: "FormControl-horizontalGroup--sm-vertical") do |_fields| %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
id: "progress-form",
html: { autocomplete: "off" },
data: { "application-target": "dynamic",
"work-packages--progress--preview-progress-target": "form",
"work-packages--progress--preview-target": "form",
controller: "work-packages--progress--focus-field " \
"work-packages--progress--preview-progress " \
"work-packages--progress--touched-field-marker" }
"work-packages--progress--preview" }
) do |f| %>
<%= flex_layout do |modal_body| %>
<% modal_body.with_row(classes: "FormControl-horizontalGroup--sm-vertical") do |_fields| %>
Expand Down
13 changes: 6 additions & 7 deletions app/forms/work_packages/pre_14_4_progress_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ def initialize(work_package:,

group.hidden(name: :status_id_touched,
value: @touched_field_map["status_id_touched"] || false,
data: { "work-packages--progress--touched-field-marker-target": "touchedFieldInput",
data: { "work-packages--progress--preview-target": "touchedFieldInput",
"referrer-field": "work_package[status_id]" })
group.hidden(name: :estimated_hours_touched,
value: @touched_field_map["estimated_hours_touched"] || false,
data: { "work-packages--progress--touched-field-marker-target": "touchedFieldInput",
data: { "work-packages--progress--preview-target": "touchedFieldInput",
"referrer-field": "work_package[estimated_hours]" })
else
render_text_field(group, name: :estimated_hours, label: I18n.t(:label_work))
Expand All @@ -105,11 +105,11 @@ def initialize(work_package:,

group.hidden(name: :estimated_hours_touched,
value: @touched_field_map["estimated_hours_touched"] || false,
data: { "work-packages--progress--touched-field-marker-target": "touchedFieldInput",
data: { "work-packages--progress--preview-target": "touchedFieldInput",
"referrer-field": "work_package[estimated_hours]" })
group.hidden(name: :remaining_hours_touched,
value: @touched_field_map["remaining_hours_touched"] || false,
data: { "work-packages--progress--touched-field-marker-target": "touchedFieldInput",
data: { "work-packages--progress--preview-target": "touchedFieldInput",
"referrer-field": "work_package[remaining_hours]" })
end
group.fields_for(:initial) do |builder|
Expand Down Expand Up @@ -195,9 +195,8 @@ def format_to_smallest_fractional_part(number)
end

def default_field_options(name)
data = { "work-packages--progress--preview-progress-target": "progressInput",
"work-packages--progress--touched-field-marker-target": "progressInput",
action: "input->work-packages--progress--touched-field-marker#markFieldAsTouched" }
data = { "work-packages--progress--preview-target": "progressInput",
action: "input->work-packages--progress--preview#markFieldAsTouched" }

if @focused_field == name
data[:"work-packages--progress--focus-field-target"] = "fieldToFocus"
Expand Down
7 changes: 3 additions & 4 deletions app/forms/work_packages/progress_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def readonly_text_field(group,
def hidden_touched_field(group, name:)
group.hidden(name: :"#{name}_touched",
value: touched(name),
data: { "work-packages--progress--touched-field-marker-target": "touchedFieldInput",
data: { "work-packages--progress--preview-target": "touchedFieldInput",
"referrer-field": "work_package[#{name}]" })
end

Expand Down Expand Up @@ -197,9 +197,8 @@ def as_percent(value)
end

def default_field_options(name)
data = { "work-packages--progress--preview-progress-target": "progressInput",
"work-packages--progress--touched-field-marker-target": "progressInput",
action: "work-packages--progress--touched-field-marker#markFieldAsTouched" }
data = { "work-packages--progress--preview-target": "progressInput",
action: "work-packages--progress--preview#markFieldAsTouched" }

if @focused_field == name
data[:"work-packages--progress--focus-field-target"] = "fieldToFocus"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def initialize(work_package:,
def hidden_initial_field(form, name:)
form.hidden(name:,
value: work_package.public_send(:"#{name}_was"),
data: { "work-packages--progress--touched-field-marker-target": "initialValueInput",
data: { "work-packages--progress--preview-target": "initialValueInput",
"referrer-field": "work_package[#{name}]" })
end
end
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,73 @@
*/

import { Controller } from '@hotwired/stimulus';
import { debounce, DebouncedFunc } from 'lodash';
import Idiomorph from 'idiomorph/dist/idiomorph.cjs';

export default class TouchedFieldMarkerController extends Controller {
interface TurboBeforeFrameRenderEventDetail {
render:(currentElement:HTMLElement, newElement:HTMLElement) => void;
}

export default class PreviewController extends Controller {
static targets = [
'form',
'progressInput',
'initialValueInput',
'touchedFieldInput',
'progressInput',
];

declare readonly progressInputTargets:HTMLInputElement[];
declare readonly formTarget:HTMLFormElement;
declare readonly initialValueInputTargets:HTMLInputElement[];
declare readonly touchedFieldInputTargets:HTMLInputElement[];
declare readonly progressInputTargets:HTMLInputElement[];

private debouncedPreview:DebouncedFunc<(event:Event) => void>;
private frameMorphRenderer:(event:CustomEvent<TurboBeforeFrameRenderEventDetail>) => void;
private targetFieldName:string;

private markFieldAsTouched(event:{ target:HTMLInputElement }) {
connect() {
this.debouncedPreview = debounce((event:Event) => { void this.preview(event); }, 100);
// TODO: Ideally morphing in this single controller should not be necessary.
// Turbo supports morphing, by adding the <turbo-frame refresh="morph"> attribute.
// However, it has a bug, and it doesn't morphs when reloading the frame via javascript.
// See https://github.com/hotwired/turbo/issues/1161 . Once the issue is solved, we can remove
// this code and just use <turbo-frame refresh="morph"> instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it be that this is now fixed with 8.0.10?

See:

https://github.com/hotwired/turbo/releases/tag/v8.0.10
hotwired/turbo#1192

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I'll test that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems I can't make it work properly. I'll merge this one, and open another PR.

this.frameMorphRenderer = (event:CustomEvent<TurboBeforeFrameRenderEventDetail>) => {
event.detail.render = (currentElement:HTMLElement, newElement:HTMLElement) => {
Idiomorph.morph(currentElement, newElement, { ignoreActiveValue: true });
};
};

this.progressInputTargets.forEach((target) => {
if (target.tagName.toLowerCase() === 'select') {
target.addEventListener('change', this.debouncedPreview);
} else {
target.addEventListener('input', this.debouncedPreview);
}
target.addEventListener('blur', this.debouncedPreview);
});

const turboFrame = this.formTarget.closest('turbo-frame') as HTMLFrameElement;
turboFrame.addEventListener('turbo:before-frame-render', this.frameMorphRenderer);
}

disconnect() {
this.debouncedPreview.cancel();
this.progressInputTargets.forEach((target) => {
if (target.tagName.toLowerCase() === 'select') {
target.removeEventListener('change', this.debouncedPreview);
} else {
target.removeEventListener('input', this.debouncedPreview);
}
target.removeEventListener('blur', this.debouncedPreview);
});
const turboFrame = this.formTarget.closest('turbo-frame') as HTMLFrameElement;
if (turboFrame) {
turboFrame.removeEventListener('turbo:before-frame-render', this.frameMorphRenderer);
}
}

markFieldAsTouched(event:{ target:HTMLInputElement }) {
this.targetFieldName = event.target.name.replace(/^work_package\[([^\]]+)\]$/, '$1');
this.markTouched(this.targetFieldName);

Expand All @@ -52,6 +104,59 @@
}
}

async preview(event:Event) {
let field:HTMLInputElement;
if (event.type === 'blur') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
field = (event as FocusEvent).relatedTarget as HTMLInputElement;
} else {
field = event.target as HTMLInputElement;
}

const form = this.formTarget;
const formData = new FormData(form) as unknown as undefined;
const formParams = new URLSearchParams(formData);
const wpParams = [
['work_package[initial][estimated_hours]', formParams.get('work_package[initial][estimated_hours]') || ''],
['work_package[initial][remaining_hours]', formParams.get('work_package[initial][remaining_hours]') || ''],
['work_package[initial][done_ratio]', formParams.get('work_package[initial][done_ratio]') || ''],
['work_package[estimated_hours]', formParams.get('work_package[estimated_hours]') || ''],
['work_package[remaining_hours]', formParams.get('work_package[remaining_hours]') || ''],
['work_package[done_ratio]', formParams.get('work_package[done_ratio]') || ''],
['work_package[status_id]', formParams.get('work_package[status_id]') || ''],
['field', field?.name ?? ''],
];

this.progressInputTargets.forEach((progressInput) => {
const touchedInputName = progressInput.name.replace(']', '_touched]');
Fixed Show fixed Hide fixed
const touchedValue = formParams.get(touchedInputName) || '';
wpParams.push([touchedInputName, touchedValue]);
});

const wpPath = this.ensureValidPathname(form.action);
const wpAction = wpPath.endsWith('/work_packages/new/progress') ? 'new' : 'edit';

const editUrl = `${wpPath}/${wpAction}?${new URLSearchParams(wpParams).toString()}`;
const turboFrame = this.formTarget.closest('turbo-frame') as HTMLFrameElement;

if (turboFrame) {
turboFrame.src = editUrl;
}
}

// Ensures that on create forms, there is an "id" for the un-persisted
// work package when sending requests to the edit action for previews.
private ensureValidPathname(formAction:string):string {
const wpPath = new URL(formAction);

if (wpPath.pathname.endsWith('/work_packages/progress')) {
// Replace /work_packages/progress with /work_packages/new/progress
wpPath.pathname = wpPath.pathname.replace('/work_packages/progress', '/work_packages/new/progress');
}

return wpPath.toString();
}

private isWorkBasedMode() {
return this.findValueInput('done_ratio') !== undefined;
}
Expand Down