Skip to content

Commit d4271b3

Browse files
ui: add alert/prompt/confirm helper function (#1018)
1 parent 2d79791 commit d4271b3

12 files changed

Lines changed: 176 additions & 196 deletions

packages/ui-default/components/dialog/index.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
/* eslint-disable react-refresh/only-export-components */
12
import $ from 'jquery';
23
import React from 'react';
34
import { i18n, tpl } from 'vj/utils';
5+
import DomainSelectAutoComplete from '../autocomplete/components/DomainSelectAutoComplete';
6+
import UserSelectAutoComplete from '../autocomplete/components/UserSelectAutoComplete';
47
import DomDialog, { DialogOptions } from './DomDialog';
58

69
export class Dialog {
@@ -108,7 +111,96 @@ export class ConfirmDialog extends Dialog {
108111
}
109112
}
110113

114+
export interface Field {
115+
type: 'text' | 'checkbox' | 'user' | 'domain';
116+
placeholder?: string;
117+
label?: string;
118+
autofocus?: boolean;
119+
required?: boolean;
120+
default?: string;
121+
}
122+
123+
export async function prompt<T extends string>(title: string, fields: Record<T, Field>) {
124+
let valueCache: Record<T, string | number | boolean> = {} as any;
125+
const defaultValues = Object.fromEntries(Object.entries(fields)
126+
.map(([name, field]: [T, Field]) => [name, field.default || ''])) as Record<T, string | number | boolean>;
127+
128+
const Component = () => {
129+
const [values, setValues] = React.useState(defaultValues);
130+
131+
React.useEffect(() => {
132+
valueCache = values;
133+
}, [values]);
134+
135+
return <div style={{ display: 'none' }}>
136+
<div className="row"><div className="columns">
137+
<h1>{title}</h1>
138+
</div></div>
139+
{Object.entries(fields).map(([name, field]: [string, Field]) => <div className="row" key={name}>
140+
<div className="columns">
141+
{['text', 'user', 'domain'].includes(field.type) && <label>
142+
{field.label}
143+
<div className="textbox-container">
144+
{field.type === 'text'
145+
? <input
146+
type="text" className="textbox" data-autofocus={field.autofocus}
147+
defaultValue={field.default}
148+
onChange={(e) => setValues({ ...values, [name]: e.target.value })}
149+
/>
150+
: field.type === 'user'
151+
? <UserSelectAutoComplete
152+
data-autofocus={field.autofocus} selectedKeys={values[name] ? [values[name]] : []}
153+
onChange={(e) => setValues({ ...values, [name]: e })}
154+
/>
155+
: <DomainSelectAutoComplete
156+
data-autofocus={field.autofocus} selectedKeys={values[name] ? [values[name]] : []}
157+
onChange={(e) => setValues({ ...values, [name]: e })}
158+
/>}
159+
</div>
160+
</label>}
161+
{field.type === 'checkbox' && <label className="checkbox">
162+
<input type="checkbox"
163+
defaultChecked={field.default === 'true'}
164+
onChange={(e) => setValues({ ...values, [name]: !!e.target.checked })}
165+
/>
166+
{field.label}
167+
</label>}
168+
</div>
169+
</div>)}
170+
</div>;
171+
};
172+
const res = await new Dialog({
173+
$body: $(tpl(<Component />, true)),
174+
onDispatch(action) {
175+
if (action === 'ok') {
176+
for (const [name, field] of Object.entries(fields)) {
177+
if ((field as any).required && !valueCache[name]) return false;
178+
}
179+
}
180+
return true;
181+
},
182+
}).open();
183+
if (res !== 'ok') return null;
184+
return valueCache;
185+
}
186+
187+
export async function confirm(text: string) {
188+
const res = await new ConfirmDialog({
189+
$body: tpl.typoMsg(text),
190+
}).open();
191+
return res === 'yes';
192+
}
193+
194+
export async function alert(text: string) {
195+
return await new InfoDialog({
196+
$body: tpl.typoMsg(text),
197+
}).open();
198+
}
199+
111200
window.Hydro.components.Dialog = Dialog;
112201
window.Hydro.components.InfoDialog = InfoDialog;
113202
window.Hydro.components.ActionDialog = ActionDialog;
114203
window.Hydro.components.ConfirmDialog = ConfirmDialog;
204+
window.Hydro.components.prompt = prompt;
205+
window.Hydro.components.confirm = confirm;
206+
window.Hydro.components.alert = alert;
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import $ from 'jquery';
2-
import { InfoDialog } from 'vj/components/dialog';
2+
import { alert } from 'vj/components/dialog';
33
import { NamedPage } from 'vj/misc/Page';
4-
import { i18n, tpl } from 'vj/utils';
4+
import { i18n } from 'vj/utils';
55

66
export default new NamedPage(['problem_create', 'problem_edit'], () => {
77
$('input[name="pid"]').on('blur', () => {
88
if (/^[0-9]+$/.test($('input[name="pid"]').val())) {
9-
new InfoDialog({
10-
$body: tpl.typoMsg(i18n('Problem ID cannot be a pure number. Leave blank if you want to use numberic id.')),
11-
}).open();
9+
alert(i18n('Problem ID cannot be a pure number. Leave blank if you want to use numberic id.'));
1210
}
1311
});
1412
});
Lines changed: 12 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,16 @@
1-
import React from 'react';
2-
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
3-
import { ActionDialog } from 'vj/components/dialog';
4-
import { i18n, tpl } from 'vj/utils';
5-
import createHint from './hint';
1+
import { prompt } from 'vj/components/dialog';
2+
import { i18n } from 'vj/utils';
63

7-
let hintInserted = false;
8-
$(tpl(
9-
<div style={{ display: 'none' }}>
10-
<div className="dialog__body--user-select">
11-
<div className="row"><div className="columns">
12-
<h1 id="select_user_hint">{i18n('Select User')}</h1>
13-
</div></div>
14-
<div className="row">
15-
<div className="columns">
16-
<label>{i18n('Username / UID')}
17-
<input name="user" type="text" className="textbox" autoComplete="off" data-autofocus />
18-
</label>
19-
</div>
20-
</div>
21-
</div>
22-
</div>,
23-
)).appendTo(document.body);
24-
const userSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--user-select [name="user"]')) as any;
25-
const userSelectDialog = new ActionDialog({
26-
$body: $('.dialog__body--user-select'),
27-
onDispatch(action) {
28-
if (action === 'ok' && userSelector.value() === null) {
29-
userSelector.focus();
30-
return false;
31-
}
32-
return true;
33-
},
34-
});
35-
36-
export default async function selectUser(hint?: string) {
37-
if (hint && !hintInserted) {
38-
createHint(hint, $('#select_user_hint'));
39-
hintInserted = true;
40-
}
41-
userSelector.clear();
42-
const action = await userSelectDialog.open();
43-
if (action !== 'ok') return null;
44-
return userSelector.value();
4+
export default async function selectUser() {
5+
const res = await prompt(i18n('Select User'), {
6+
user: {
7+
type: 'user',
8+
label: i18n('Username / UID'),
9+
required: true,
10+
autofocus: true,
11+
},
12+
});
13+
return res?.user;
4514
}
4615

4716
window.Hydro.components.selectUser = selectUser;

packages/ui-default/pages/contest_edit.page.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import moment from 'moment';
33
import LanguageSelectAutoComplete from 'vj/components/autocomplete/LanguageSelectAutoComplete';
44
import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete';
55
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
6-
import { ConfirmDialog } from 'vj/components/dialog';
6+
import { confirm } from 'vj/components/dialog';
77
import { NamedPage } from 'vj/misc/Page';
8-
import { i18n, request, tpl } from 'vj/utils';
8+
import { i18n, request } from 'vj/utils';
99

1010
const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => {
1111
ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false });
@@ -42,12 +42,11 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create',
4242
});
4343
}
4444
const message = `Confirm deleting this ${pagename.split('_')[0]}? Its files and status will be deleted as well.`;
45-
return new ConfirmDialog({
46-
$body: tpl.typoMsg(i18n(message)),
47-
}).open().then((action) => {
48-
if (action !== 'yes') return;
49-
confirmed = true;
50-
ev.target.click();
45+
confirm(i18n(message)).then((yes) => {
46+
if (yes) {
47+
confirmed = true;
48+
ev.target.click();
49+
}
5150
});
5251
});
5352
setInterval(() => {

packages/ui-default/pages/domain_user.page.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import $ from 'jquery';
22
import _ from 'lodash';
33
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
4-
import { ActionDialog, ConfirmDialog, InfoDialog } from 'vj/components/dialog';
4+
import { ActionDialog, confirm, InfoDialog } from 'vj/components/dialog';
55
import Notification from 'vj/components/notification';
66
import { NamedPage } from 'vj/misc/Page';
77
import {
@@ -101,14 +101,8 @@ const page = new NamedPage('domain_user', () => {
101101
if (selectedUsers === null) {
102102
return;
103103
}
104-
const action = await new ConfirmDialog({
105-
$body: tpl`
106-
<div class="typo">
107-
<p>${i18n('Confirm removing the selected users?')}</p>
108-
<p>${i18n('Their account will not be deleted and they will be with the guest role until they re-join the domain.')}</p>
109-
</div>`,
110-
}).open();
111-
if (action !== 'yes') return;
104+
if (!(await confirm(`${i18n('Confirm removing the selected users?')}
105+
${i18n('Their account will not be deleted and they will be with the guest role until they re-join the domain.')}`))) return;
112106
try {
113107
await request.post('', {
114108
operation: 'kick',

packages/ui-default/pages/problem_config.page.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import $ from 'jquery';
33
import yaml from 'js-yaml';
44
import React from 'react';
55
import { createRoot } from 'react-dom/client';
6-
import { ConfirmDialog } from 'vj/components/dialog/index';
6+
import { confirm, prompt } from 'vj/components/dialog/index';
77
import Notification from 'vj/components/notification';
88
import { configYamlFormat } from 'vj/components/problemconfig/ProblemConfigEditor';
99
import uploadFiles from 'vj/components/upload';
1010
import download from 'vj/components/zipDownloader';
1111
import { NamedPage } from 'vj/misc/Page';
1212
import {
13-
i18n, loadReactRedux, pjax, request, tpl,
13+
i18n, loadReactRedux, pjax, request,
1414
} from 'vj/utils';
1515

1616
const page = new NamedPage('problem_config', () => {
@@ -41,8 +41,9 @@ const page = new NamedPage('problem_config', () => {
4141

4242
async function handleClickRename(ev: JQuery.ClickEvent<Document, undefined, any, any>) {
4343
const file = [$(ev.currentTarget).parent().parent().attr('data-filename')];
44-
// eslint-disable-next-line no-alert
45-
const newName = prompt(i18n('Enter a new name for the file: '));
44+
const newName = (await prompt(i18n('Enter a new name for the file: '), {
45+
name: { required: true, type: 'text', autofocus: true },
46+
}))?.name as string;
4647
if (!newName) return;
4748
try {
4849
await request.post('./files', {
@@ -60,10 +61,7 @@ const page = new NamedPage('problem_config', () => {
6061

6162
async function handleClickRemove(ev: JQuery.ClickEvent<Document, undefined, any, any>) {
6263
const file = [$(ev.currentTarget).parent().parent().attr('data-filename')];
63-
const action = await new ConfirmDialog({
64-
$body: tpl.typoMsg(i18n('Confirm to delete the file?')),
65-
}).open();
66-
if (action !== 'yes') return;
64+
if (!(await confirm(i18n('Confirm to delete the file?')))) return;
6765
try {
6866
await request.post('./files', {
6967
operation: 'delete_files',

packages/ui-default/pages/problem_detail.page.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import $ from 'jquery';
22
import yaml from 'js-yaml';
33
import React from 'react';
44
import { createRoot } from 'react-dom/client';
5-
import { ConfirmDialog } from 'vj/components/dialog';
5+
import { confirm } from 'vj/components/dialog';
66
import Notification from 'vj/components/notification';
77
import { downloadProblemSet } from 'vj/components/zipDownloader';
88
import { NamedPage } from 'vj/misc/Page';
@@ -316,10 +316,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
316316
</a>
317317
</li>));
318318
$(document).on('click', '#clearAnswers', async () => {
319-
const result = await new ConfirmDialog({
320-
$body: tpl.typoMsg(i18n('All changes will be lost. Are you sure to clear all answers?')),
321-
}).open();
322-
if (result === 'yes') await clearAns();
319+
if (await confirm(i18n('All changes will be lost. Are you sure to clear all answers?'))) await clearAns();
323320
});
324321
}
325322
const ele = document.createElement('div');

packages/ui-default/pages/problem_files.page.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { map } from 'lodash';
44
import React from 'react';
55
import ReactDOM from 'react-dom/client';
66
import FileSelectAutoComplete from 'vj/components/autocomplete/FileSelectAutoComplete';
7-
import { ActionDialog, ConfirmDialog, InfoDialog } from 'vj/components/dialog/index';
7+
import { ActionDialog, confirm, InfoDialog } from 'vj/components/dialog/index';
88
import createHint from 'vj/components/hint';
99
import Notification from 'vj/components/notification';
1010
import { previewFile } from 'vj/components/preview/preview.page';
@@ -255,10 +255,7 @@ const page = new NamedPage('problem_files', () => {
255255

256256
async function handleClickRemove(ev, type) {
257257
const file = [$(ev.currentTarget).parent().parent().attr('data-filename')];
258-
const action = await new ConfirmDialog({
259-
$body: tpl.typoMsg(i18n('Confirm to delete the file?')),
260-
}).open();
261-
if (action !== 'yes') return;
258+
if (!(await confirm(i18n('Confirm to delete the file?')))) return;
262259
try {
263260
await request.post('./files', {
264261
operation: 'delete_files',
@@ -275,10 +272,7 @@ const page = new NamedPage('problem_files', () => {
275272
async function handleClickRemoveSelected(type) {
276273
const selectedFiles = ensureAndGetSelectedFiles(type);
277274
if (selectedFiles === null) return;
278-
const action = await new ConfirmDialog({
279-
$body: tpl.typoMsg(i18n('Confirm to delete the selected files?')),
280-
}).open();
281-
if (action !== 'yes') return;
275+
if (!(await confirm(i18n('Confirm to delete the selected files?')))) return;
282276
try {
283277
await request.post('', {
284278
operation: 'delete_files',

0 commit comments

Comments
 (0)