mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: add group bulk import/export
This commit is contained in:
5 files changed
+101
-3
No files matched your search
@@ -114,7 +114,7 @@ export class ConfirmDialog extends Dialog {
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
type: 'text' | 'checkbox' | 'user' | 'userId' | 'username' | 'domain';
|
||||
type: 'text' | 'textarea' | 'checkbox' | 'user' | 'userId' | 'username' | 'domain';
|
||||
options?: string[] | Record<string, string>;
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
@@ -122,10 +122,11 @@ export interface Field {
|
||||
required?: boolean;
|
||||
default?: string;
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
type Result<T extends string, R extends Record<T, Field>> = {
|
||||
[K in keyof R]: R[K]['type'] extends ('text' | 'password' | 'username' | 'domain') ? string
|
||||
[K in keyof R]: R[K]['type'] extends ('text' | 'password' | 'username' | 'domain' | 'textarea') ? string
|
||||
: R[K]['type'] extends 'checkbox' ? boolean
|
||||
: R[K]['type'] extends 'userId' ? number
|
||||
: R[K]['type'] extends 'user' ? any
|
||||
@@ -175,6 +176,18 @@ export async function prompt<T extends string, R extends Record<T, Field>>(title
|
||||
</div></div>
|
||||
{layout.map((i) => <div className="row" key={i[0][0]}>
|
||||
{i.map(([name, field]: [string, Field]) => <div key={name} className={`columns medium-${Math.abs(field.columns || 12)}`}>
|
||||
{field.type === 'textarea' && <label>
|
||||
{field.label}
|
||||
<textarea
|
||||
className="textbox"
|
||||
rows={field.rows || 6}
|
||||
placeholder={field.placeholder}
|
||||
defaultValue={field.default}
|
||||
data-autofocus={field.autofocus}
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
onChange={(e) => setValues({ ...values, [name]: e.target.value })}
|
||||
/>
|
||||
</label>}
|
||||
{['text', 'user', 'userId', 'username', 'domain'].includes(field.type) && <label>
|
||||
{field.label}
|
||||
<div className="textbox-container">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
||||
import { ActionDialog, ConfirmDialog } from 'vj/components/dialog';
|
||||
import { ActionDialog, ConfirmDialog, prompt } from 'vj/components/dialog';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import {
|
||||
@@ -132,7 +132,76 @@ const page = new NamedPage('domain_group', () => {
|
||||
Notification.success(i18n('Saved.'));
|
||||
}
|
||||
|
||||
async function handleClickImportGroups() {
|
||||
const result = await prompt(i18n('Import Groups'), {
|
||||
groups: {
|
||||
type: 'textarea',
|
||||
label: i18n('Format: one group per line, CSV: groupName,uid1,uid2,...'),
|
||||
rows: 10,
|
||||
required: true,
|
||||
placeholder: 'group1,1001,1002,1003\ngroup2,1004,1005',
|
||||
},
|
||||
});
|
||||
if (!result) return;
|
||||
const lines = String(result.groups || '').replace(/^\uFEFF/, '').split('\n');
|
||||
const parsed = new Map<string, number[]>();
|
||||
const errors: string[] = [];
|
||||
lines.forEach((raw, idx) => {
|
||||
const line = raw.trim();
|
||||
if (!line) return;
|
||||
const [name, ...rest] = line.split(',').map((t) => t.trim());
|
||||
if (!name) return;
|
||||
const uids: number[] = [];
|
||||
for (const t of rest) {
|
||||
if (!t) continue;
|
||||
const uid = +t;
|
||||
if (!Number.isInteger(uid)) {
|
||||
errors.push(i18n('Line {0}: Invalid UID "{1}".', idx + 1, t));
|
||||
return;
|
||||
}
|
||||
uids.push(uid);
|
||||
}
|
||||
parsed.set(name, Array.from(new Set(uids)));
|
||||
});
|
||||
if (errors.length) {
|
||||
Notification.error(errors.join('\n'));
|
||||
return;
|
||||
}
|
||||
if (!parsed.size) {
|
||||
Notification.error(i18n('No groups to import.'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
for (const [name, uids] of parsed) {
|
||||
await update(name, uids);
|
||||
}
|
||||
Notification.success(i18n('Imported successfully.'));
|
||||
await delay(1500);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClickExportGroups() {
|
||||
const rows: string[] = [];
|
||||
for (const gid of Object.keys(targets)) {
|
||||
const uids = targets[gid].value() as number[];
|
||||
rows.push([gid, ...uids].join(','));
|
||||
}
|
||||
await prompt(i18n('Export Groups'), {
|
||||
groups: {
|
||||
type: 'textarea',
|
||||
label: i18n('Copy the content below'),
|
||||
rows: 15,
|
||||
default: rows.join('\n'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$('[name="create_group"]').click(() => handleClickCreateGroup());
|
||||
$('[name="import_groups"]').click(() => handleClickImportGroups());
|
||||
$('[name="export_groups"]').click(() => handleClickExportGroups());
|
||||
$('[name="remove_selected"]').click(() => handleClickDeleteSelected());
|
||||
$('[name="save_all"]').on('click', () => handleClickSaveAll());
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('{0}: Groups').format(domain.name) }}</h1>
|
||||
<div class="section__tools">
|
||||
<button class="rounded button" name="import_groups">{{ _('Import Groups') }}</button>
|
||||
<button class="rounded button" name="export_groups">{{ _('Export Groups') }}</button>
|
||||
<button class="primary rounded button" name="create_group">{{ _('Create Group') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user