diff --git a/shared/src/model/time.ts b/shared/src/model/time.ts index 5759de1a9d..97a984c04b 100644 --- a/shared/src/model/time.ts +++ b/shared/src/model/time.ts @@ -12,7 +12,8 @@ import { now as getNow } from 'mobx-utils' const { range } = extendMoment(moment as any); -//console.log({ range }) +// https://moment.github.io/luxon/docs/manual/formatting +type LocaleFormat = 'DATE_SHORT' | 'DATE_MED' | 'DATE_MED_WITH_WEEKDAY' | 'DATE_FULL' | 'DATE_HUGE' | 'TIME_SIMPLE' | 'TIME_WITH_SECONDS' | 'TIME_WITH_SHORT_OFFSET' | 'TIME_WITH_LONG_OFFSET' | 'TIME_24_SIMPLE' | 'TIME_24_WITH_SECONDS' | 'TIME_24_WITH_SHORT_OFFSET' | 'TIME_24_WITH_LONG_OFFSET' | 'DATETIME_SHORT' | 'DATETIME_MED' | 'DATETIME_FULL' | 'DATETIME_HUGE' | 'DATETIME_SHORT_WITH_SECONDS' | 'DATETIME_MED_WITH_SECONDS' | 'DATETIME_FULL_WITH_SECONDS' | 'DATETIME_HUGE_WITH_SECONDS' let shiftMs = 0; const defaultResolution = 1000 * 60; // one minute resolution @@ -42,6 +43,8 @@ export type { DurationUnit } export type ComparableValue = Date | Time | LDT export type TimeInputs = Time | Date | string | number | LDT +export type DATE_TIME_FORMAT = keyof typeof LDT // DateTimeFormatOptions + export default class Time { static get now() { @@ -131,6 +134,8 @@ export default class Time { get isUnknown() { return this._value === Time.unknown._value } get isValid() { return Boolean(!this.isUnknown && this._value.isValid) } + toLocaleString(fmt: LocaleFormat) { return this._value.toLocaleString(LDT[fmt]) } + toFormat(fmt: string) { return this._value.toFormat(fmt) } // left for comatibility with momentjs, do not use for new code format(fmt: string) { return this.asMoment.format(fmt) } diff --git a/tutor/specs/global.d.ts b/tutor/specs/global.d.ts index 7083c845f3..b35eac97e8 100644 --- a/tutor/specs/global.d.ts +++ b/tutor/specs/global.d.ts @@ -11,4 +11,13 @@ interface TutorTestConfig { declare global { const testConfig: TutorTestConfig interface Window { _MODELS: any; } + + namespace jest { + interface Matchers { + toHaveRendered(match: string): R + } + interface Expect { + snapshot(reactEl: any): JestMatchers + } + } } diff --git a/tutor/specs/screens/course-settings/__snapshots__/details.spec.tsx.snap b/tutor/specs/screens/course-settings/__snapshots__/details.spec.tsx.snap new file mode 100644 index 0000000000..cea91ad00a --- /dev/null +++ b/tutor/specs/screens/course-settings/__snapshots__/details.spec.tsx.snap @@ -0,0 +1,415 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Course Settings, course details matches snapshot 1`] = ` +.c4 { + margin-right: 0.5rem; + margin-left: 0.5rem; + -webkit-transition: color 0.3s ease; + transition: color 0.3s ease; +} + +.c3 { + max-width: 25rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c1 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; +} + +.c1 .dropdown-item { + white-space: break-spaces; +} + +.c2.c2.c2 { + border: 1px solid #d5d5d5; + color: #424242; + background: #fff; + height: 3.4rem; + width: 25rem; + text-align: left; + padding: 0 1rem; + font-size: 1.4rem; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: 0.4rem; +} + +.c2.c2.c2:focus { + border-color: #0dc0dc; + box-shadow: 0 0 4px 0 rgba(13,192,220,0.5); +} + +.c2.c2.c2:after { + color: #818181; + -webkit-flex-basis: 0; + -ms-flex-preferred-size: 0; + flex-basis: 0; + font-size: 24px; +} + +.c2.c2.c2.has-error { + border-color: #e298a0; + background-color: #f8e8ea; +} + +.c0.c0.c0 form .form-controls { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-flex-flow: row wrap; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + width: 75%; +} + +.c0.c0.c0 form .form-controls .btn.btn-primary { + padding: 0.75rem 2rem; +} + +.c0.c0.c0 form .form-controls .btn.btn-primary.hidden { + display: none; +} + +.c0.c0.c0 form .form-controls .btn.btn-link { + margin-left: -15px; + padding-top: 15px; +} + +.c0.c0.c0 form .form-group { + margin-bottom: 2.4rem; +} + +.c0.c0.c0 form .form-group.row { + margin-left: 0; +} + +.c0.c0.c0 form .form-group .dropdown { + width: 100%; +} + +.c0.c0.c0 form .form-group .dropdown .dropdown-toggle { + height: 45px; + width: 25%; +} + +.c0.c0.c0 form .form-group .dropdown .dropdown-menu { + min-width: 25%; +} + +.c0.c0.c0 form .form-group span.error-message { + color: red; + margin-top: 2px; +} + +.c0.c0.c0 form .form-group label, +.c0.c0.c0 form .form-group .timezone-label { + font-weight: 700; + font-size: 1.4rem; + line-height: 2rem; + padding: 10px 0; +} + +.c0.c0.c0 form .form-group .form-control { + height: 45px; + font-size: 1.6rem; + width: 25%; +} + +.c0.c0.c0 form .form-group .form-control.error-input { + background: #fbe7ea; + color: red; + border-color: #f4c0c5; + border-width: 2px; +} + +.c0.c0.c0 form .form-group .form-control#course-name { + width: 60%; +} + +.c0.c0.c0 form .form-group .form-control#endDate, +.c0.c0.c0 form .form-group .form-control#startDate { + width: 57%; +} + +.c0.c0.c0 form .form-group.dates .end-date-label { + margin-left: -15rem; +} + +.c0.c0.c0 form hr { + width: 80%; + margin: 0; + margin-bottom: 1.5rem; +} + +.c0.c0.c0 .disabled-delete-course { + color: #027EB5; + opacity: 40%; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + font-weight: 500; + font-size: 1.4rem; +} + +@media (max-width:999px) { + .c0.c0.c0 form .form-group .dropdown .dropdown-toggle { + width: 100%; + } +} + +@media (max-width:999px) { + .c0.c0.c0 form .form-group .dropdown .dropdown-menu { + width: 100%; + } +} + +@media (max-width:999px) { + .c0.c0.c0 form .form-group .form-control#course-name { + width: 100%; + } +} + +@media (max-width:999px) { + .c0.c0.c0 form .form-group .form-control#endDate, + .c0.c0.c0 form .form-group .form-control#startDate { + width: 100%; + } +} + +@media (max-width:999px) { + .c0.c0.c0 form .form-group .form-control { + width: 100%; + } +} + +@media (max-width:999px) { + .c0.c0.c0 form .form-group.dates .end-date-label { + margin-left: 0; + } +} + +@media (max-width:999px) { + .c0.c0.c0 form hr { + width: 100%; + } +} + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+
+ + Time zone + +
+
+
+ +
+
+
+
+
+ + +
+
+
+`; diff --git a/tutor/specs/screens/course-settings/details.spec.tsx b/tutor/specs/screens/course-settings/details.spec.tsx new file mode 100644 index 0000000000..e2c1dce092 --- /dev/null +++ b/tutor/specs/screens/course-settings/details.spec.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import CourseDetails from '../../../src/screens/course-settings/course-details'; +import { Factory } from '../../helpers' +import type { Course } from '../../../src/models' +import { mount } from 'enzyme' + +describe('Course Settings, course details', () => { + let props: any; + let course: Course; + + beforeEach(() => { + course = Factory.course() + props = { + history: {}, + course, + }; + }); + + it('matches snapshot', () => { + expect.snapshot().toMatchSnapshot(); + }) + it('renders with correct dates', () => { + const settings = mount(); + expect(settings).toHaveRendered( + `input[name="startDate"][value="${course.starts_at.toLocaleString('DATE_MED')}"]` + ) + expect(settings).toHaveRendered( + `input[name="endDate"][value="${course.ends_at.toLocaleString('DATE_MED')}"]` + ) + settings.unmount() + }); + +}) diff --git a/tutor/src/screens/course-settings/course-details.js b/tutor/src/screens/course-settings/course-details.js index f565abf484..8f25dd395d 100644 --- a/tutor/src/screens/course-settings/course-details.js +++ b/tutor/src/screens/course-settings/course-details.js @@ -1,10 +1,10 @@ -import { React, PropTypes, moment, styled, cn, observer, useState, action } from 'vendor'; +import { React, PropTypes, styled, cn, observer, useState, action } from 'vendor'; import { Form, Row, Col } from 'react-bootstrap'; import { Formik } from 'formik'; import { isEmpty } from 'lodash'; import DeleteCourseModal from './delete-course-button'; import Timezone from './timezone'; -import { Course, currentToasts } from '../../models' +import { Course, currentToasts, Time } from '../../models' import { AsyncButton } from 'shared'; import { colors, breakpoint } from 'theme'; @@ -115,7 +115,7 @@ const StyledCourseDetails = styled.div` } `; -const df = (d) => moment(d).format('MMM DD, YYYY'); +const df = (d) => new Time(d).toLocaleString('DATE_MED') const CourseDetails = observer(({ course, history }) => { const [isSaving, setIsSaving] = useState(false); @@ -221,7 +221,7 @@ const CourseDetails = observer(({ course, history }) => {