Skip to content

Commit 2e7176f

Browse files
authored
Merge pull request adfinis#767 from Yelinz/fix-detepicker
fix(ember): fix membership datepickers
2 parents bf6df19 + 41657bd commit 2e7176f

File tree

14 files changed

+214
-52
lines changed

14 files changed

+214
-52
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { action } from "@ember/object";
2+
import { inject as service } from "@ember/service";
3+
import Component from "@glimmer/component";
4+
import { tracked } from "@glimmer/tracking";
5+
import { DateTime } from "luxon";
6+
7+
export default class CfFieldInputDateComponent extends Component {
8+
@service intl;
9+
10+
@tracked flatpickrRef = null;
11+
12+
get locale() {
13+
return this.intl.primaryLocale;
14+
}
15+
16+
@action
17+
onReady(_selectedDates, _dateStr, flatpickrRef) {
18+
this.flatpickrRef = flatpickrRef;
19+
}
20+
21+
@action
22+
clearCalendar(e) {
23+
e.stopPropagation();
24+
e.preventDefault();
25+
this.flatpickrRef.clear();
26+
}
27+
28+
@action
29+
onChange([date]) {
30+
// Change Javascript date to ISO string if not null.
31+
this.args.onChange(date ? DateTime.fromJSDate(date).toISODate() : null);
32+
}
33+
34+
// flatpickr doesnt call onChange after manual input and clicking outside.
35+
@action
36+
onClose(dates) {
37+
this.onChange(dates);
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div class="uk-inline uk-width-1-1">
2+
<a
3+
class="uk-form-icon uk-form-icon-flip"
4+
uk-tooltip={{t "caluma.form.delete"}}
5+
href="#"
6+
{{on "click" this.clearCalendar}}
7+
>
8+
<UkIcon @icon="close" />
9+
</a>
10+
<EmberFlatpickr
11+
class="uk-input {{if @disabled 'uk-disabled'}}"
12+
readonly={{@disabled}}
13+
@disabled={{@disabled}}
14+
@locale={{this.locale}}
15+
@date={{or @date null}}
16+
@altFormat="d.m.Y"
17+
@altInput={{true}}
18+
@allowInput={{true}}
19+
@onChange={{this.onChange}}
20+
@onReady={{this.onReady}}
21+
@onClose={{this.onClose}}
22+
/>
23+
</div>

ember/app/ui/components/identity-memberships/component.js

+34-27
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ export default class IdentityMembershipsComponent extends Component {
1919
@service intl;
2020
@service notification;
2121

22-
get locale() {
23-
return this.intl.primaryLocale;
24-
}
25-
2622
// List
2723

2824
@lastValue("fetchMemberships") memberships;
@@ -41,14 +37,19 @@ export default class IdentityMembershipsComponent extends Component {
4137
this.fetchRoles.perform(this.args.identity);
4238
}
4339

40+
@action updateDateField(fieldName, newValue, changeset) {
41+
changeset.rollbackProperty(fieldName);
42+
changeset.set(fieldName, newValue);
43+
}
44+
4445
@action
4546
openPowerSelect(select) {
4647
select.actions.open();
4748
}
4849

4950
// Add / Edit
5051

51-
@tracked changeset = null;
52+
@tracked changeset;
5253

5354
@action
5455
edit(membership) {
@@ -72,38 +73,44 @@ export default class IdentityMembershipsComponent extends Component {
7273
this.changeset = null;
7374
}
7475

76+
formatDate(date) {
77+
// Flatpickr returns an array of dates.
78+
if (Array.isArray(date)) {
79+
return DateTime.fromJSDate(date[0]).toISODate();
80+
} else if (date) {
81+
return date;
82+
}
83+
return null;
84+
}
85+
7586
@dropTask
7687
*submit(changeset) {
7788
try {
78-
const format = "yyyy-LL-dd";
79-
let timeSlot = changeset.get("timeSlot") || {};
80-
if (!timeSlot.lower && !timeSlot.upper) {
81-
timeSlot = null;
82-
} else {
83-
timeSlot.lower = timeSlot.lower
84-
? DateTime.fromJSDate(timeSlot.lower[0]).toFormat(format)
85-
: undefined;
86-
timeSlot.upper = timeSlot.upper
87-
? DateTime.fromJSDate(timeSlot.upper[0]).toFormat(format)
88-
: undefined;
89+
changeset.execute();
90+
const timeSlot = new Map(Object.entries(changeset.data.timeSlot));
91+
timeSlot.forEach((slot) => {
92+
if (slot) {
93+
const [key, value] = slot;
94+
timeSlot[key] = this.formatDate(value);
95+
}
96+
});
97+
if (timeSlot.size) {
98+
changeset.set("timeSlot", Object.fromEntries(timeSlot));
8999
}
90-
changeset.set("timeSlot", timeSlot);
91-
92100
const election = changeset.get("nextElection");
93-
if (election) {
94-
changeset.set(
95-
"nextElection",
96-
DateTime.fromJSDate(election[0]).toFormat(format)
97-
);
101+
changeset.set("nextElection", this.formatDate(election));
102+
changeset.set("timeSlot.bounds", "[)");
103+
yield changeset.validate();
104+
if (changeset.isValid) {
105+
yield changeset.save();
106+
yield this.onUpdate();
107+
this.changeset = null;
98108
}
99-
100-
yield changeset.save();
101-
this.changeset = null;
102-
yield this.onUpdate();
103109
} catch (error) {
104110
console.error(error);
105111
this.notification.fromError(error);
106112
applyError(changeset, error);
113+
changeset.rollback();
107114
}
108115
}
109116

ember/app/ui/components/identity-memberships/template.hbs

+14-24
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,13 @@
7979

8080
{{#if (can "add membership-role")}}
8181
<p class="uk-text-right">
82-
<button
83-
class="uk-button uk-button-default"
84-
type="button"
85-
disabled={{this.changeset}}
86-
{{on "click" (fn this.edit null)}}
82+
<UkButton
83+
@disabled={{this.changeset}}
84+
@onClick={{fn this.edit null}}
85+
data-test-add
8786
>
8887
{{t "components.identity-memberships.add"}}
89-
</button>
88+
</UkButton>
9089
</p>
9190
{{/if}}
9291
{{/if}}
@@ -121,6 +120,7 @@
121120
@onChange={{field.update}}
122121
@onFocus={{this.openPowerSelect}}
123122
@triggerClass="uk-select"
123+
data-test-organisation-select
124124
as |opt|
125125
>
126126
{{opt.organisationName}}
@@ -165,15 +165,12 @@
165165
<Form.input
166166
@label={{t "components.identity-memberships.form.label.timeFrom"}}
167167
@name="timeSlot.lower"
168+
@errorComponent={{component "validated-form-error"}}
168169
as |field|
169170
>
170-
<EmberFlatpickr
171-
class="uk-input"
172-
@date={{if field.value field.value null}}
171+
<DatePicker
172+
@date={{field.value}}
173173
@onChange={{field.update}}
174-
@dateFormat="d.m.Y"
175-
@locale={{this.locale}}
176-
@allowInput={{true}}
177174
/>
178175
</Form.input>
179176
</div>
@@ -183,15 +180,12 @@
183180
<Form.input
184181
@label={{t "components.identity-memberships.form.label.timeUntil"}}
185182
@name="timeSlot.upper"
183+
@errorComponent={{component "validated-form-error"}}
186184
as |field|
187185
>
188-
<EmberFlatpickr
189-
class="uk-input"
190-
@date={{if field.value field.value null}}
186+
<DatePicker
187+
@date={{field.value}}
191188
@onChange={{field.update}}
192-
@dateFormat="d.m.Y"
193-
@locale={{this.locale}}
194-
@allowInput={{true}}
195189
/>
196190
</Form.input>
197191
</div>
@@ -205,13 +199,9 @@
205199
@name="nextElection"
206200
as |field|
207201
>
208-
<EmberFlatpickr
209-
class="uk-input"
210-
@date={{if field.value field.value null}}
202+
<DatePicker
203+
@date={{field.value}}
211204
@onChange={{field.update}}
212-
@dateFormat="d.m.Y"
213-
@locale={{this.locale}}
214-
@allowInput={{true}}
215205
/>
216206
</Form.input>
217207
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<small class="uk-text-danger">
2+
{{yield}}
3+
{{#each @errors as |error idx|}}
4+
{{if (not (eq idx 0)) ", "}} {{!-- Space out multiple errors --}}
5+
{{t error}}
6+
{{/each}}
7+
</small>

ember/app/utils/apply-error.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export default function applyError(changeset, error) {
22
error.errors
3-
.filter(
3+
?.filter(
44
({ source: { pointer } }) =>
55
pointer.startsWith("/data/attributes") &&
66
!pointer.endsWith("non-field-errors")

ember/app/validations/membership.js

+42
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,47 @@
11
import { validatePresence } from "ember-changeset-validations/validators";
2+
import { DateTime } from "luxon";
3+
4+
function validateTimeSlotLower() {
5+
return (key, newValue, oldValue, changes, content) => {
6+
if (
7+
newValue &&
8+
(changes?.timeSlot?.upper ?? content.get("timeSlot.upper"))
9+
) {
10+
if (
11+
DateTime.fromISO(newValue) >
12+
DateTime.fromISO(
13+
changes?.timeSlot?.upper ?? content.get("timeSlot.upper")
14+
)
15+
) {
16+
return "components.identity-memberships.timeSlotErrorLower";
17+
}
18+
}
19+
return true;
20+
};
21+
}
22+
23+
function validateTimeSlotUpper() {
24+
return (key, newValue, oldValue, changes, content) => {
25+
if (
26+
newValue &&
27+
(changes?.timeSlot?.lower ?? content.get("timeSlot.lower"))
28+
) {
29+
if (
30+
DateTime.fromISO(
31+
changes?.timeSlot?.lower ?? content.get("timeSlot.lower")
32+
) > DateTime.fromISO(newValue)
33+
) {
34+
return "components.identity-memberships.timeSlotError";
35+
}
36+
}
37+
return true;
38+
};
39+
}
240

341
export default {
442
organisation: [validatePresence(true)],
43+
timeSlot: {
44+
lower: [validateTimeSlotLower()],
45+
upper: [validateTimeSlotUpper()],
46+
},
547
};

ember/config/environment.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module.exports = function (environment) {
2727
allowAnyFallback: true,
2828
fallbacks: ["de", "en"],
2929
},
30+
"changeset-validations": { rawOutput: true },
3031
apollo: {
3132
apiURL: "/graphql",
3233
},

ember/ember-cli-build.js

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ module.exports = function (defaults) {
1111
},
1212
"ember-validated-form": {
1313
theme: "uikit",
14+
defaults: {
15+
"types/date": "myapp/components/date-picker",
16+
},
1417
},
1518
"ember-fetch": {
1619
preferNative: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { click, fillIn, render } from "@ember/test-helpers";
2+
import { hbs } from "ember-cli-htmlbars";
3+
import { module, test } from "qunit";
4+
5+
import { setupRenderingTest } from "mysagw/tests/helpers";
6+
7+
module("Integration | Component | date-picker", function (hooks) {
8+
setupRenderingTest(hooks);
9+
10+
test("it renders", async function (assert) {
11+
this.onChange = () => {
12+
assert.step("onChange");
13+
};
14+
15+
await render(hbs`<DatePicker @onChange={{this.onChange}}/>`);
16+
17+
await fillIn('input[type="text"]', "03.04.2020");
18+
19+
assert.dom('input[type="text"]').hasValue("03.04.2020");
20+
21+
await click("a");
22+
23+
assert.dom("input").hasValue("");
24+
assert.verifySteps(["onChange", "onChange"]);
25+
});
26+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { render } from "@ember/test-helpers";
2+
import { hbs } from "ember-cli-htmlbars";
3+
import { setupIntl } from "ember-intl/test-support";
4+
import { module, test } from "qunit";
5+
6+
import { setupRenderingTest } from "mysagw/tests/helpers";
7+
8+
module("Integration | Component | validated-form-error", function (hooks) {
9+
setupRenderingTest(hooks);
10+
setupIntl(hooks);
11+
12+
test("it renders", async function (assert) {
13+
this.errors = ["error1", "error2"];
14+
await render(hbs`<ValidatedFormError @errors={{this.errors}}/>`);
15+
16+
assert.dom(this.element).hasText("t:error1:() , t:error2:()");
17+
});
18+
});

ember/translations/components/identity-memberships/de.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ components:
44
add: "Mitgliedschaft hinzufügen"
55
timeSlotLower: "Von"
66
timeSlotUpper: "Bis"
7+
timeSlotError: "Das Bis-Datum kann nicht vor dem Von-Datum liegen."
8+
timeSlotErrorLower: "Das Von-Datum kann nicht nach dem Bis-Datum liegen."
79

810
list:
911
nextElection: "Nächste Wiederwahl {date}"

ember/translations/components/identity-memberships/en.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ components:
44
add: "Add membership"
55
timeSlotLower: "From"
66
timeSlotUpper: "Until"
7+
timeSlotError: "Until date can't be before From date."
8+
timeSlotErrorLower: "From date can't be after Until date."
79

810
list:
911
nextElection: "Next election {date}"

0 commit comments

Comments
 (0)