diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 76b52463e52e..c35189593f18 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -4,13 +4,10 @@ export type EdgePagedResponse = PagedResponse & { last_evaluated_key?: string pages?: (string | undefined)[] } -export type Approval = - | { - user: number - } - | { - group: number - } +export type Approval = { + user?: number + group?: number +} export type PagedResponse = { count?: number next?: string diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 142b23423f40..ff194b667b25 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -87,7 +87,7 @@ "react": "16.14.0", "react-async-script": "1.2.0", "react-click-outside": "3.0.1", - "react-datepicker": "6.2.0", + "react-datepicker": "^8.1.0", "react-device-detect": "1.9.9", "react-diff-viewer-continued": "^3.3.1", "react-dom": "16.14.0", @@ -2906,40 +2906,26 @@ ] }, "node_modules/@floating-ui/core": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz", - "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==", + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", "dependencies": { - "@floating-ui/utils": "^0.2.7" + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.10", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz", - "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==", + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", "dependencies": { "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.7" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.26.23", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.23.tgz", - "integrity": "sha512-9u3i62fV0CFF3nIegiWiRDwOs7OW/KhSUJDNx2MkQM3LbE5zQOY01sL3nelcVBXvX7Ovvo3A49I8ql+20Wg/Hw==", - "dependencies": { - "@floating-ui/react-dom": "^2.1.1", - "@floating-ui/utils": "^0.2.7", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", - "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", "dependencies": { "@floating-ui/dom": "^1.0.0" }, @@ -2949,9 +2935,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz", - "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==" + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" }, "node_modules/@gar/promisify": { "version": "1.1.3", @@ -7850,9 +7836,9 @@ } }, "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -15898,35 +15884,40 @@ "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" }, "node_modules/react-datepicker": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-6.2.0.tgz", - "integrity": "sha512-GzEOiE6yLfp9P6XNkOhXuYtZHzoAx3tirbi7/dj2WHlGM+NGE1lefceqGR0ZrYsYaqsNJhIJFTgwUpzVzA+mjw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.1.0.tgz", + "integrity": "sha512-11gIOrBGK1MOvl4+wxGv4YxTqXf+uoRPtKstYhb/P1cBdRdOP1sL26VE31apmDnvw8wSYfJe9AWwWbKqmM9tzw==", "dependencies": { - "@floating-ui/react": "^0.26.2", - "classnames": "^2.2.6", - "date-fns": "^3.3.1", - "prop-types": "^15.7.2", - "react-onclickoutside": "^6.13.0" + "@floating-ui/react": "^0.27.3", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" }, "peerDependencies": { - "react": "^16.9.0 || ^17 || ^18", - "react-dom": "^16.9.0 || ^17 || ^18" + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/react-datepicker/node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "node_modules/react-datepicker/node_modules/@floating-ui/react": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.4.tgz", + "integrity": "sha512-05mXdkUiVh8NCEcYKQ2C9SV9IkZ9k/dFtYmaEIN2riLv80UHoXylgBM76cgPJYfLJM3dJz7UE5MOVH0FypMd2Q==", "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.9", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" } }, - "node_modules/react-datepicker/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "node_modules/react-datepicker/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } }, "node_modules/react-device-detect": { "version": "1.9.9", @@ -16114,19 +16105,6 @@ "react": "^0.14.9 || ^15.3.0 || ^16.0.0" } }, - "node_modules/react-onclickoutside": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz", - "integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==", - "funding": { - "type": "individual", - "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" - }, - "peerDependencies": { - "react": "^15.5.x || ^16.x || ^17.x || ^18.x", - "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" - } - }, "node_modules/react-popper": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 826fbeb786f2..116cbd96c1d0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -111,7 +111,7 @@ "react": "16.14.0", "react-async-script": "1.2.0", "react-click-outside": "3.0.1", - "react-datepicker": "6.2.0", + "react-datepicker": "^8.1.0", "react-device-detect": "1.9.9", "react-diff-viewer-continued": "^3.3.1", "react-dom": "16.14.0", diff --git a/frontend/web/components/AdminAPIKeys.js b/frontend/web/components/AdminAPIKeys.js index 38a0f9bdafa4..f2cd198001a6 100644 --- a/frontend/web/components/AdminAPIKeys.js +++ b/frontend/web/components/AdminAPIKeys.js @@ -273,7 +273,7 @@ export class CreateAPIKey extends PureComponent { { this.setState({ - expiry_date: e.toISOString(), + expiry_date: e?.toISOString(), }) }} selected={expiry_date ? moment(expiry_date)._d : null} diff --git a/frontend/web/components/DateSelect.js b/frontend/web/components/DateSelect.tsx similarity index 56% rename from frontend/web/components/DateSelect.js rename to frontend/web/components/DateSelect.tsx index f122def36b69..b66e6eaca5fb 100644 --- a/frontend/web/components/DateSelect.js +++ b/frontend/web/components/DateSelect.tsx @@ -1,15 +1,39 @@ -import DatePicker from 'react-datepicker' +import DatePicker, { DatePickerProps } from 'react-datepicker' import Icon from './Icon' -import { useState } from 'react' +import { useState, FC } from 'react' + +export interface DateSelectProps + extends Pick { + value?: DatePickerProps['value'] + className?: string + isValid?: boolean + onChange?: ( + date: Date | null, + event?: React.MouseEvent | React.KeyboardEvent, + ) => void +} + +const DateSelect: FC = ({ + className, + dateFormat, + isValid, + onChange, + selected, + value, +}) => { + const [isMonthPicker, setIsMonthPicker] = useState(false) + const [isYearPicker, setIsYearPicker] = useState(false) + const [touched, setTouched] = useState(false) + const [isOpen, setIsOpen] = useState(false) -const DateSelect = ({ dateFormat, onChange, onSelect, selected, value }) => { - const [isMonthPicker, setMonthPicker] = useState(false) - const [isYearPicker, setYearPicker] = useState(false) - const [isOpen, setOpen] = useState(false) return ( setTouched(true)} renderCustomHeader={({ date, decreaseMonth, @@ -32,9 +56,9 @@ const DateSelect = ({ dateFormat, onChange, onSelect, selected, value }) => {
{ - setMonthPicker(true) + setIsMonthPicker(true) if (isMonthPicker) { - setYearPicker(true) + setIsYearPicker(true) } }} className='react-datepicker-header-title' @@ -64,25 +88,33 @@ const DateSelect = ({ dateFormat, onChange, onSelect, selected, value }) => { )} minDate={new Date()} - onChange={(date, e) => { - if (date < new Date()) { - setMonthPicker(false) - setYearPicker(false) - onChange(new Date()) - } else { - onChange(date) - if (!e) { - setOpen(false) - } - if (isMonthPicker) { - setMonthPicker(false) - } - if (isYearPicker) { - setYearPicker(false) - } + onChange={(date, e): DatePickerProps['onChange'] => { + if (date === null) { + setIsMonthPicker(false) + setIsYearPicker(false) + onChange?.(null) + return + } + + const today = new Date() + if (date < today) { + setIsMonthPicker(false) + setIsYearPicker(false) + onChange?.(new Date()) + return + } + + onChange?.(date) + if (!e) { + setIsOpen(false) + } + if (isMonthPicker) { + setIsMonthPicker(false) + } + if (isYearPicker) { + setIsYearPicker(false) } }} - className='input-lg' formatWeekDay={(nameOfDay) => nameOfDay.substr(0, 1)} showTimeSelect={!isMonthPicker && !isYearPicker} showMonthYearPicker={isMonthPicker} @@ -92,12 +124,12 @@ const DateSelect = ({ dateFormat, onChange, onSelect, selected, value }) => { selected={selected} value={value} timeFormat='HH:mm' - onInputClick={() => setOpen(true)} + onInputClick={() => setIsOpen(true)} onClickOutside={(e) => { if (e) { - setOpen(false) - setMonthPicker(false) - setYearPicker(false) + setIsOpen(false) + setIsMonthPicker(false) + setIsYearPicker(false) } }} open={isOpen} diff --git a/frontend/web/components/modals/ChangeRequestModal.js b/frontend/web/components/modals/ChangeRequestModal.js deleted file mode 100644 index 8993b6fb6171..000000000000 --- a/frontend/web/components/modals/ChangeRequestModal.js +++ /dev/null @@ -1,287 +0,0 @@ -import React, { Component } from 'react' -import UserSelect from 'components/UserSelect' -import OrganisationProvider from 'common/providers/OrganisationProvider' -import Button from 'components/base/forms/Button' -import MyGroupsSelect from 'components/MyGroupsSelect' -import { getMyGroups } from 'common/services/useMyGroup' -import { getStore } from 'common/store' -import DateSelect from 'components/DateSelect' -import { close } from 'ionicons/icons' -import { IonIcon } from '@ionic/react' -import InfoMessage from 'components/InfoMessage' - -const ChangeRequestModal = class extends Component { - static displayName = 'ChangeRequestModal' - - static contextTypes = { - router: propTypes.object.isRequired, - } - - state = { - approvals: - (this.props.changeRequest && this.props.changeRequest.approvals) || [], - description: - (this.props.changeRequest && this.props.changeRequest.description) || '', - groups: [], - live_from: - (this.props.changeRequest && - this.props.changeRequest.feature_states[0].live_from) || - undefined, - title: (this.props.changeRequest && this.props.changeRequest.title) || '', - } - - componentDidMount() { - getMyGroups(getStore(), { orgId: AccountStore.getOrganisation().id }).then( - (res) => { - this.setState({ groups: res?.data?.results || [] }) - }, - ) - } - - addOwner = (id, isUser = true) => { - this.setState({ - approvals: (this.state.approvals || []).concat( - isUser ? { user: id } : { group: id }, - ), - }) - } - - removeOwner = (id, isUser = true) => { - this.setState({ - approvals: (this.state.approvals || []).filter((v) => - isUser ? v.user !== id : v.group !== id, - ), - }) - } - - getApprovals = (users, approvals) => - users.filter((v) => approvals.find((a) => a.user === v.id)) - - getGroupApprovals = (groups, approvals) => - groups.filter((v) => approvals.find((a) => a.group === v.id)) - - save = () => { - const { approvals, description, live_from, title } = this.state - this.props.onSave({ - approvals, - description, - live_from: live_from || undefined, - title, - }) - } - - render() { - const { description, groups, title } = this.state - return ( - - {({ users }) => { - const ownerGroups = this.getGroupApprovals( - groups, - this.state.approvals || [], - ) - const ownerUsers = this.getApprovals( - users, - this.state.approvals || [], - ) - return ( -
- - - this.setState({ title: Utils.safeParseEventValue(e) }) - } - isValid={title && title.length} - type='text' - title='Title' - inputProps={{ className: 'full-width' }} - className='full-width' - placeholder='My Change Request' - /> - - - - this.setState({ description: Utils.safeParseEventValue(e) }) - } - type='text' - title='Description' - inputProps={{ - className: 'full-width', - style: { minHeight: 80 }, - }} - className='full-width' - placeholder='Add an optional description...' - /> - -
- - { - this.setState({ - live_from: e.toISOString(), - }) - }} - selected={moment(this.state.live_from)._d} - /> - - - - } - /> -
- {moment(this.state.live_from).isAfter(moment()) && ( - - This change will be scheduled to go live at{' '} - - {moment(this.state.live_from).format('Do MMM YYYY hh:mma')}{' '} - ({Intl.DateTimeFormat().resolvedOptions().timeZone} Time). - - - )} - {!this.props.changeRequest && - this.props.showAssignees && - !Utils.getFlagsmithHasFeature('disable_users_as_reviewers') && ( - - - {!Utils.getFlagsmithHasFeature( - 'disable_users_as_reviewers', - ) && ( - - Users: - {ownerUsers.map((u) => ( - this.removeOwner(u.id)} - className='chip' - style={{ marginBottom: 4, marginTop: 4 }} - > - - {u.first_name} {u.last_name} - - - - - - ))} - - - )} - - Groups: - {ownerGroups.map((u) => ( - this.removeOwner(u.id, false)} - className='chip' - style={{ marginBottom: 4, marginTop: 4 }} - > - - {u.name} - - - - - - ))} - - -
- } - onChange={(e) => - this.setState({ - description: Utils.safeParseEventValue(e), - }) - } - type='text' - title='Assignees' - tooltipPlace='top' - tooltip='Assignees will be able to review and approve the change request' - inputProps={{ - className: 'full-width', - style: { minHeight: 80 }, - }} - className='full-width' - placeholder='Add an optional description...' - /> - - )} - {!this.props.changeRequest && - !Utils.getFlagsmithHasFeature('disable_users_as_reviewers') && ( - v.id !== AccountStore.getUser().id, - )} - value={this.state.approvals.map((v) => v.user)} - onAdd={this.addOwner} - onRemove={this.removeOwner} - isOpen={this.state.showUsers} - onToggle={() => - this.setState({ showUsers: !this.state.showUsers }) - } - /> - )} - {!this.props.changeRequest && ( - v.group)} - onAdd={this.addOwner} - onRemove={this.removeOwner} - isOpen={this.state.showGroups} - onToggle={() => - this.setState({ showGroups: !this.state.showGroups }) - } - /> - )} - - - - -
- ) - }} - - ) - } -} - -export default ChangeRequestModal diff --git a/frontend/web/components/modals/ChangeRequestModal.tsx b/frontend/web/components/modals/ChangeRequestModal.tsx new file mode 100644 index 000000000000..395b3a793d0f --- /dev/null +++ b/frontend/web/components/modals/ChangeRequestModal.tsx @@ -0,0 +1,301 @@ +import React, { useState, useEffect, FC, useMemo } from 'react' +import UserSelect from 'components/UserSelect' +import OrganisationProvider from 'common/providers/OrganisationProvider' +import Button from 'components/base/forms/Button' +import MyGroupsSelect from 'components/MyGroupsSelect' +import { useGetMyGroupsQuery } from 'common/services/useMyGroup' +import DateSelect, { DateSelectProps } from 'components/DateSelect' +import { close } from 'ionicons/icons' +import { IonIcon } from '@ionic/react' +import InfoMessage from 'components/InfoMessage' +import AccountStore from 'common/stores/account-store' +import InputGroup from 'components/base/forms/InputGroup' +import moment from 'moment' +import Utils from 'common/utils/utils' +import { Approval, ChangeRequest, User } from 'common/types/responses' +import { Req } from 'common/types/requests' + +interface ChangeRequestModalProps { + changeRequest?: ChangeRequest + onSave: ( + data: Omit, + ) => void + isScheduledChange?: boolean + showAssignees?: boolean +} + +const ChangeRequestModal: FC = ({ + changeRequest, + isScheduledChange, + onSave, + showAssignees, +}) => { + const [approvals, setApprovals] = useState([ + ...(changeRequest?.approvals ?? []), + ...(changeRequest?.group_assignments ?? []), + ]) + const [description, setDescription] = useState( + String(changeRequest?.description ?? ''), + ) + // const [groups, setGroups] = useState([]) + const [liveFrom, setLiveFrom] = useState( + changeRequest?.feature_states[0]?.live_from, + ) + const [title, setTitle] = useState(changeRequest?.title ?? '') + const [showUsers, setShowUsers] = useState(false) + const [showGroups, setShowGroups] = useState(false) + const [currDate, setCurrDate] = useState(new Date()) + + const { data: groups } = useGetMyGroupsQuery({ + orgId: AccountStore.getOrganisation().id, + }) + + useEffect(() => { + const currLiveFromDate = changeRequest?.feature_states[0]?.live_from + if (!currLiveFromDate) { + return setLiveFrom(showAssignees ? currDate.toISOString() : undefined) + } + }, [isScheduledChange, showAssignees, changeRequest, currDate]) + + const addOwner = (id: number, isUser = true) => { + if (!isUser) { + setApprovals((prev) => [...prev, { group: id }]) + return + } + + setApprovals((prev) => [...prev, { user: id }]) + } + + const removeOwner = (id: number, isUser = true) => { + if (!isUser) { + setApprovals((prev) => prev.filter((v) => v.group !== id)) + return + } + + setApprovals((prev) => prev.filter((v) => v.user !== id)) + } + + const getApprovals = (users: User[], approvals: Approval[]) => + users.filter((u) => approvals.find((a) => a.user === u.id)) + + const ownerGroups = useMemo( + () => + groups?.results?.filter((g) => approvals.find((a) => a.group === g.id)), + [groups?.results, approvals], + ) + + const save = () => { + onSave({ + approvals, + description, + live_from: liveFrom || undefined, + title, + }) + } + + const handleClear = () => { + const newCurrDate = new Date() + setCurrDate(newCurrDate) + setLiveFrom(showAssignees ? newCurrDate.toISOString() : undefined) + } + + const handleOnDateChange: DateSelectProps['onChange'] = (date) => { + setLiveFrom(date?.toISOString()) + } + + const isValid = useMemo(() => { + return !!title?.length && !!liveFrom + }, [title, liveFrom]) + + return ( + + {({ users }) => { + const ownerUsers = getApprovals(users, approvals) + return ( +
+ + setTitle(Utils.safeParseEventValue(e))} + isValid={!!title && title.length > 0} + type='text' + title='Title' + inputProps={{ className: 'full-width' }} + className='full-width' + placeholder='My Change Request' + /> + + + + setDescription(Utils.safeParseEventValue(e)) + } + type='text' + title='Description' + inputProps={{ + className: 'full-width', + style: { minHeight: 80 }, + }} + className='full-width' + placeholder='Add an optional description...' + /> + +
+ + + + + } + /> +
+ {showAssignees && moment(liveFrom).isSame(currDate) && ( + + + Changes will take effect immediately after approval. + + + )} + {liveFrom && moment(liveFrom).isAfter(moment()) && ( + + This change will be scheduled to go live at{' '} + + {moment(liveFrom).format('Do MMM YYYY hh:mma')} ( + {Intl.DateTimeFormat().resolvedOptions().timeZone} Time). + + + )} + {!changeRequest && + showAssignees && + !Utils.getFlagsmithHasFeature('disable_users_as_reviewers') && ( + + + {!Utils.getFlagsmithHasFeature( + 'disable_users_as_reviewers', + ) && ( + + Users: + {ownerUsers.map((u) => ( + removeOwner(u.id)} + className='chip' + style={{ marginBottom: 4, marginTop: 4 }} + > + + {u.first_name} {u.last_name} + + + + + + ))} + + + )} + + Groups: + {ownerGroups?.map((u) => ( + removeOwner(u.id, false)} + className='chip' + style={{ marginBottom: 4, marginTop: 4 }} + > + {u.name} + + + + + ))} + + +
+ } + onChange={(e: Event) => + setDescription(Utils.safeParseEventValue(e)) + } + type='text' + title='Assignees' + tooltipPlace='top' + tooltip='Assignees will be able to review and approve the change request' + inputProps={{ + className: 'full-width', + style: { minHeight: 80 }, + }} + className='full-width' + placeholder='Add an optional description...' + /> + + )} + {!changeRequest && + !Utils.getFlagsmithHasFeature('disable_users_as_reviewers') && ( + v.id !== AccountStore.getUser().id, + )} + value={approvals.map((v) => v.user)} + onAdd={addOwner} + onRemove={removeOwner} + isOpen={showUsers} + onToggle={() => setShowUsers(!showUsers)} + /> + )} + {!changeRequest && ( + Number(v.group))} + onAdd={addOwner} + onRemove={removeOwner} + isOpen={showGroups} + onToggle={() => setShowGroups(!showGroups)} + size='-sm' + /> + )} + + + + + ) + }} +
+ ) +} + +export default ChangeRequestModal diff --git a/frontend/web/components/modals/CreateFlag.js b/frontend/web/components/modals/CreateFlag.js index 6761fa413d13..ec49e2444d1c 100644 --- a/frontend/web/components/modals/CreateFlag.js +++ b/frontend/web/components/modals/CreateFlag.js @@ -967,6 +967,7 @@ const CreateFlag = class extends Component { : 'New Change Request',