Skip to content

Commit

Permalink
feat: show alert modal before leaving general settings page
Browse files Browse the repository at this point in the history
  • Loading branch information
a110605 committed Apr 29, 2024
1 parent 16393cb commit 7c04370
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 38 deletions.
45 changes: 45 additions & 0 deletions src/routes/setting/LeaveSettingsModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Icon } from 'antd'
import { ModalBlur } from '../../components'

const modal = ({
visible,
onCancel,
onOk,
changedSettings,
}) => {
const modalOpts = {
title: 'Leave General Settings ?',
visible,
onCancel,
onOk,
okText: 'Leave',
}

return (
<ModalBlur {...modalOpts}>
<p type="warning">
<Icon style={{ marginRight: '10px' }} type="exclamation-circle" />
You have unsaved changes below.
</p>
<ul>
{Object.keys(changedSettings).sort().map((key) => (
<li key={key}>
{`${key}`} : <strong>{`${changedSettings[key]}`}</strong>
</li>
))}
</ul>
<p>Are you sure you want to leave this page?</p>
</ModalBlur>
)
}

modal.propTypes = {
visible: PropTypes.bool,
onCancel: PropTypes.func,
onOk: PropTypes.func,
changedSettings: PropTypes.object,
}

export default modal
119 changes: 103 additions & 16 deletions src/routes/setting/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,119 @@ import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'dva'
import SettingForm from './setting'
import LeaveSettingsModal from './LeaveSettingsModal'
import { Prompt } from 'dva/router'
class Setting extends React.Component {
constructor(props) {
super(props)
this.state = {
changedSettings: {},
modalVisible: false,
nextLocation: null,
confirmedNavigation: false,
}
}

showModal = (location) => this.setState({
modalVisible: true,
nextLocation: location,
})

closeModal = (callback) => this.setState({
modalVisible: false,
}, callback)

handleBlockedNavigation = (nextLocation) => {
const { confirmedNavigation, changedSettings } = this.state
const isDirty = Object.keys(changedSettings).length > 0

if (nextLocation.pathname !== '/settings' && isDirty && !confirmedNavigation) {
this.showModal(nextLocation)
return false // disallow navigation
}
return true // allow navigation
}

function Setting({ setting, dispatch, loading }) {
const { data, saving } = setting
const props = {
data,
saving,
loading,
onSubmit(payload) {
dispatch({
type: 'setting/update',
payload,
handleConfirmNavigationClick = () => this.closeModal(() => {
const { history } = this.props
const { nextLocation } = this.state
if (nextLocation) {
this.setState({
confirmedNavigation: true,
}, () => {
history.push(nextLocation.pathname)
})
},
}
})

onInputChange = (displayName, newValue) => {
const { setting: { data } } = this.props
const targetSettingOldValue = data.find(d => d.definition.displayName === displayName)?.value
if (targetSettingOldValue && targetSettingOldValue.toString() !== newValue.toString()) {
this.setState(prevState => ({
changedSettings: {
...prevState.changedSettings,
[displayName]: newValue,
},
}))
} else {
this.setState(prevState => {
const prevChangedSettings = { ...prevState.changedSettings }
if (displayName in prevChangedSettings) {
delete prevChangedSettings[displayName]
}
return {
changedSettings: {
...prevChangedSettings,
},
}
})
}
}

return (
<div className="content-inner">
<SettingForm {...props} />
</div>
)
render() {
const { setting, dispatch, loading } = this.props
const { modalVisible, changedSettings } = this.state
const { data, saving } = setting

const settingFormProps = {
data,
saving,
loading,
onSubmit(payload) {
dispatch({
type: 'setting/update',
payload,
})
},
onInputChange: this.onInputChange,
}

return (
<div className="content-inner" style={{ overflow: 'hidden' }}>
<SettingForm {...settingFormProps} />
<Prompt
when
message={this.handleBlockedNavigation}
/>
{modalVisible && (
<LeaveSettingsModal
visible={modalVisible}
onCancel={() => this.closeModal()}
onOk={this.handleConfirmNavigationClick}
changedSettings={changedSettings}
/>
)}
</div>
)
}
}

Setting.propTypes = {
setting: PropTypes.object,
dispatch: PropTypes.func,
loading: PropTypes.bool,
history: PropTypes.object,
}

export default connect(({ setting, loading }) => ({ setting, loading: loading.models.setting }))(Setting)
64 changes: 42 additions & 22 deletions src/routes/setting/setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const form = ({
},
data,
saving,
loading,
onSubmit,
onInputChange,
}) => {
const handleOnSubmit = () => {
const fields = getFieldsValue()
Expand All @@ -39,20 +41,26 @@ const form = ({
}
}
const genInputItem = (setting) => {
const settingType = setting.definition.type
const settingName = setting.definition.displayName

if (setting.definition && setting.definition.options) {
return (<Select getPopupContainer={triggerNode => triggerNode.parentElement}>
{setting.definition.options.map((item, index) => {
return <Option key={index} value={item}>{item}</Option>
})}
</Select>)
return (
<Select onChange={value => onInputChange(settingName, value)} getPopupContainer={triggerNode => triggerNode.parentElement}>
{setting.definition.options.map((item, index) => (
<Option key={index} value={item}>{item}</Option>
))}
</Select>
)
}
switch (setting.definition.type) {

switch (settingType) {
case 'bool':
return (<Checkbox disabled={setting.definition.readOnly} />)
return (<Checkbox disabled={setting.definition.readOnly} onChange={e => onInputChange(settingName, e.target.checked)} />)
case 'int':
return (<InputNumber style={{ width: '100%' }} parser={limitNumber} disabled={setting.definition.readOnly} min={0} />)
return (<InputNumber onChange={value => onInputChange(settingName, value)} style={{ width: '100%' }} parser={limitNumber} disabled={setting.definition.readOnly} min={0} />)
default:
return (<Input readOnly={setting.definition.readOnly} />)
return (<Input readOnly={setting.definition.readOnly} onChange={e => onInputChange(settingName, e.target.value)} />)
}
}
const genFormItem = (setting) => {
Expand Down Expand Up @@ -94,6 +102,7 @@ const form = ({
</FormItem>
)
}

const getCategoryWeight = (category) => {
switch (category) {
case 'general':
Expand Down Expand Up @@ -128,22 +137,32 @@ const form = ({
return 1
}
return 0
}).map(item => <div key={item}> <div className={classnames(styles.fieldset, { [styles.dangerZone]: item === 'danger Zone' })}><span className={styles.fieldsetLabel}>{item}</span> { settingsGrouped[item].map(setting => genFormItem(setting))}</div></div>)
}).map(item => (
<div key={item}>
<div className={classnames(styles.fieldset, { [styles.dangerZone]: item === 'danger Zone' })}>
<span className={styles.fieldsetLabel}>{item}</span>
{settingsGrouped[item].map(setting => genFormItem(setting))}
</div>
</div>
))

return (
<Spin spinning={saving}>
{<Form className={styles.setting}>
<Spin spinning={saving || loading}>
<Form className={styles.setting}>
{settings}
<FormItem style={{ textAlign: 'center' }}>
<Button
onClick={handleOnSubmit}
loading={saving}
type="primary"
htmlType="submit">
Save
</Button>
</FormItem>
</Form>}
</Form>
{settings.length > 0 && (
<div style={{ textAlign: 'center', position: 'sticky', marginTop: '1rem' }}>
<Button
onClick={handleOnSubmit}
loading={saving}
type="primary"
htmlType="submit"
>
Save
</Button>
</div>
)}
</Spin>
)
}
Expand All @@ -154,6 +173,7 @@ form.propTypes = {
onSubmit: PropTypes.func,
saving: PropTypes.bool,
loading: PropTypes.bool,
onInputChange: PropTypes.func,
}

export default Form.create()(form)
7 changes: 7 additions & 0 deletions src/routes/setting/setting.less
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.setting {
max-height: 78vh;
overflow-y: auto;

:global .ant-input-group-addon {
text-transform: capitalize;
}
Expand All @@ -25,3 +28,7 @@
border: 1px solid #f15354;
}
}

:global .ant-spin-nested-loading{
height: 100vh;
}

0 comments on commit 7c04370

Please sign in to comment.