mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
ui: domain_group management
This commit is contained in:
1 parent
8144e85c14
commit
b1c63ddd6c
17 files changed
+270
-58
No files matched your search
@@ -2,8 +2,8 @@ __id: en
|
||||
__langname: English
|
||||
Can be Extended For: Extension
|
||||
contest_create: Contest Create
|
||||
contest_detail_problem: Problem Detail
|
||||
contest_detail_problem_submit: Submit Contest Problem
|
||||
contest_detail_problem: Problem Detail
|
||||
contest_detail: Contest Detail
|
||||
contest_edit: Contest Edit
|
||||
contest_main: Contest
|
||||
@@ -17,6 +17,7 @@ display_name: Display Name
|
||||
domain_dashboard: Dashboard
|
||||
domain_discussion: Discussion Nodes
|
||||
domain_edit: Edit Info
|
||||
domain_group: Manage Groups
|
||||
domain_join_applications: Join Applications
|
||||
domain_main: Main
|
||||
domain_permission: System Permission
|
||||
|
||||
@@ -259,6 +259,7 @@ domain_dashboard: 管理域
|
||||
domain_discussion: 讨论节点
|
||||
domain_edit: 编辑域资料
|
||||
domain_file: 我的文件
|
||||
domain_group: 管理小组
|
||||
domain_join_applications: 加域申请
|
||||
domain_join: 加入域
|
||||
domain_main: 首页
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "3.3.8",
|
||||
"version": "3.3.9",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/loader",
|
||||
"module": "src/loader",
|
||||
|
||||
@@ -27,9 +27,9 @@ registerValue('GroupInfo', [
|
||||
]);
|
||||
|
||||
registerResolver('Query', 'domain(id: String)', 'Domain', async (args, ctx) => {
|
||||
const ddoc = await domain.get(args.id);
|
||||
const ddoc = args.id ? await domain.get(args.id) : ctx.domain;
|
||||
if (!ddoc) return null;
|
||||
const udoc = await user.getById(args.id, ctx.user._id);
|
||||
const udoc = await user.getById(ddoc._id, ctx.user._id);
|
||||
if (!udoc.hasPerm(PERM.PERM_VIEW) && !udoc.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) return null;
|
||||
ctx.udoc = udoc;
|
||||
return ddoc;
|
||||
@@ -37,7 +37,7 @@ registerResolver('Query', 'domain(id: String)', 'Domain', async (args, ctx) => {
|
||||
|
||||
registerResolver('Domain', 'manage', 'DomainManage', async (args, ctx) => {
|
||||
if (!ctx.udoc.hasPerm(PERM.PERM_EDIT_DOMAIN)) throw new PermissionError(PERM.PERM_EDIT_DOMAIN);
|
||||
return {};
|
||||
return ctx.parent;
|
||||
});
|
||||
|
||||
registerResolver('DomainManage', 'group', 'DomainGroup', (args, ctx) => ctx.parent);
|
||||
@@ -267,8 +267,12 @@ class DomainJoinApplicationsHandler extends ManageHandler {
|
||||
}
|
||||
|
||||
class DomainUserGroupHandler extends ManageHandler {
|
||||
async get() {
|
||||
this.response.body = 'domain_group.html';
|
||||
async get({ domainId }) {
|
||||
this.response.template = 'domain_group.html';
|
||||
this.response.body = {
|
||||
domain: this.domain,
|
||||
groups: await user.listGroup(domainId),
|
||||
};
|
||||
}
|
||||
|
||||
@param('name', Types.Name)
|
||||
|
||||
@@ -392,7 +392,7 @@ class UserModel {
|
||||
}
|
||||
|
||||
static updateGroup(domainId: string, name: string, uids: number[]) {
|
||||
return collGroup.updateOne({ domainId, name }, { $set: { uids } });
|
||||
return collGroup.updateOne({ domainId, name }, { $set: { uids } }, { upsert: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ if (process.env.NODE_APP_INSTANCE === '0') {
|
||||
{ upsert: true },
|
||||
);
|
||||
feedback();
|
||||
setInterval(update, 60 * 1000);
|
||||
setInterval(update, 1800 * 1000);
|
||||
});
|
||||
const taskColl = db.collection('task');
|
||||
let taskCount = await taskColl.find().count();
|
||||
|
||||
+15
-7
@@ -23,16 +23,24 @@ function monitorResource(resource) {
|
||||
|
||||
export default class DOMAttachedObject {
|
||||
static uniqueIdCounter = 0;
|
||||
static DOMAttachKey: string;
|
||||
static DOMAttachSelector: string;
|
||||
static DOMDetachSelector: string;
|
||||
// FIXME should be JQuery<HTMLElement>, but typings for $.transition() wasn't found
|
||||
$dom: any;
|
||||
id: number;
|
||||
detached: boolean;
|
||||
eventNS: string;
|
||||
|
||||
static get($obj) {
|
||||
const key = this.DOMAttachKey;
|
||||
return $obj.data(key);
|
||||
}
|
||||
|
||||
static getOrConstruct($obj, ...args) {
|
||||
static getOrConstruct<T = DOMAttachedObject>($obj, ...args): T {
|
||||
const $singleObj = $obj.eq(0);
|
||||
const key = this.DOMAttachKey;
|
||||
const Protoclass = this;
|
||||
const Protoclass = this as any;
|
||||
const instance = this.get($singleObj);
|
||||
if (instance !== undefined) return instance;
|
||||
const newInstance = new Protoclass($singleObj, ...args);
|
||||
@@ -43,7 +51,7 @@ export default class DOMAttachedObject {
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
static attachAll(container = document.body, ...args) {
|
||||
static attachAll(container: Document | HTMLElement = document.body, ...args) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!this.DOMAttachSelector) {
|
||||
// eslint-disable-next-line quotes
|
||||
@@ -63,7 +71,7 @@ export default class DOMAttachedObject {
|
||||
}
|
||||
}
|
||||
|
||||
static detachAll(container = document.body) {
|
||||
static detachAll(container: Document | HTMLElement = document.body) {
|
||||
const selector = this.DOMDetachSelector || this.DOMAttachSelector;
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!selector) {
|
||||
@@ -104,13 +112,13 @@ export default class DOMAttachedObject {
|
||||
}
|
||||
|
||||
detach() {
|
||||
if (this.constructor.DOMAttachKey) {
|
||||
this.$dom.removeData(this.constructor.DOMAttachKey);
|
||||
if ((this.constructor as any).DOMAttachKey) {
|
||||
this.$dom.removeData((this.constructor as any).DOMAttachKey);
|
||||
}
|
||||
this.detached = true;
|
||||
}
|
||||
|
||||
constructor($dom, monitorDetach = false) {
|
||||
constructor($dom: JQuery<HTMLElement>, monitorDetach = false) {
|
||||
if ($dom == null) return null;
|
||||
this.$dom = $dom;
|
||||
this.id = ++DOMAttachedObject.uniqueIdCounter;
|
||||
+15
-15
@@ -3,31 +3,31 @@ import ReactDOM from 'react-dom';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { assign } from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import AutoComplete from '.';
|
||||
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||
import UserSelectAutoCompleteFC from './components/UserSelectAutoComplete';
|
||||
|
||||
export default class UserSelectAutoComplete extends AutoComplete {
|
||||
export default class UserSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
||||
static DOMAttachKey = 'ucwUserSelectAutoCompleteInstance';
|
||||
client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
constructor($dom, options) {
|
||||
constructor($dom, options: AutoCompleteOptions<Multi>) {
|
||||
super($dom, {
|
||||
classes: 'user-select',
|
||||
...options,
|
||||
});
|
||||
this.client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
value() {
|
||||
value(): Multi extends true ? number[] : string {
|
||||
if (this.options.multi) return this.ref?.getSelectedItems().map((item) => item._id) ?? this.$dom.val();
|
||||
return this.ref?.getSelectedItems()[0] ?? null;
|
||||
}
|
||||
@@ -5,30 +5,18 @@ import { useQuery } from 'react-query';
|
||||
import PropTypes from 'prop-types';
|
||||
import api, { gql } from 'vj/utils/api';
|
||||
import AutoComplete from './AutoComplete';
|
||||
|
||||
interface UserSelectAutoCompleteProps {
|
||||
multi?: boolean;
|
||||
defaultItems?: string;
|
||||
width: string,
|
||||
height: string,
|
||||
listStyle: any,
|
||||
onChange: (value) => any,
|
||||
allowEmptyQuery: boolean,
|
||||
freeSolo: boolean,
|
||||
freeSoloConverter: any,
|
||||
}
|
||||
import type { AutoCompleteOptions } from '..';
|
||||
|
||||
// eslint-disable-next-line prefer-arrow-callback
|
||||
const UserSelectAutoComplete = forwardRef(function UserSelectAutoComplete(
|
||||
props: UserSelectAutoCompleteProps, ref: React.ForwardedRef<AutoComplete>,
|
||||
props: AutoCompleteOptions, ref: React.ForwardedRef<AutoComplete>,
|
||||
) {
|
||||
const multi = props.multi ?? false;
|
||||
const rawDefaultItems = props.defaultItems ?? '';
|
||||
const defaultItems = multi ? rawDefaultItems
|
||||
.split(',')
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((i) => +i) : rawDefaultItems;
|
||||
.map((i) => +i.trim())
|
||||
.filter((i) => i) : rawDefaultItems;
|
||||
|
||||
const comRef = useRef<AutoComplete>();
|
||||
const itemsBox = useRef([]);
|
||||
|
||||
+25
-5
@@ -4,8 +4,29 @@ import { assign } from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import AutoCompleteFC from './components/AutoComplete';
|
||||
|
||||
export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
||||
multi?: Multi;
|
||||
defaultItems?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
listStyle?: any;
|
||||
allowEmptyQuery?: boolean;
|
||||
freeSolo?: boolean;
|
||||
freeSoloConverter?: any;
|
||||
onChange?: (value) => any;
|
||||
items?: () => Promise<any[]>;
|
||||
render?: () => string;
|
||||
text?: () => string;
|
||||
}
|
||||
|
||||
export default class AutoComplete extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'ucwAutoCompleteInstance';
|
||||
ref = null;
|
||||
container = document.createElement('div');
|
||||
options: AutoCompleteOptions;
|
||||
changeListener = [
|
||||
(val) => this.$dom.val(val),
|
||||
];
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
super($dom);
|
||||
@@ -16,7 +37,6 @@ export default class AutoComplete extends DOMAttachedObject {
|
||||
multi: false,
|
||||
...options,
|
||||
};
|
||||
this.ref = null;
|
||||
this.clear = this.clear.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.attach = this.attach.bind(this);
|
||||
@@ -25,7 +45,6 @@ export default class AutoComplete extends DOMAttachedObject {
|
||||
this.value = this.value.bind(this);
|
||||
this.detach = this.detach.bind(this);
|
||||
this.focus = this.focus.bind(this);
|
||||
this.container = document.createElement('div');
|
||||
this.$dom.addClass('autocomplete-dummy').after(this.container);
|
||||
// Note: use `setTimeout(fn, 0)` to ensure that code is executed after browser autofill
|
||||
// also see https://stackoverflow.com/a/779785/13553984
|
||||
@@ -38,8 +57,9 @@ export default class AutoComplete extends DOMAttachedObject {
|
||||
else this.ref.closeList();
|
||||
}
|
||||
|
||||
onChange(val) {
|
||||
this.$dom.val(val);
|
||||
onChange(val: string | ((v: string) => any)) {
|
||||
if (typeof val === 'string') this.changeListener.forEach((f) => f(val));
|
||||
else this.changeListener.push(val);
|
||||
}
|
||||
|
||||
attach() {
|
||||
@@ -70,7 +90,7 @@ export default class AutoComplete extends DOMAttachedObject {
|
||||
this.ref.closeList();
|
||||
}
|
||||
|
||||
value() {
|
||||
value(): any {
|
||||
if (this.options.multi) return this.$dom.val();
|
||||
return this.ref?.getSelectedItems()[0] ?? null;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import zIndexManager from 'vj/utils/zIndexManager';
|
||||
|
||||
export interface DialogOptions {
|
||||
classes: string;
|
||||
$body: HTMLElement;
|
||||
$body: HTMLElement | JQuery<HTMLElement> | string;
|
||||
$action: any;
|
||||
width?: string;
|
||||
height?: string;
|
||||
|
||||
@@ -83,6 +83,11 @@ export class ActionDialog extends Dialog {
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.$dom.find('input').val('');
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfirmDialog extends Dialog {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.29.5",
|
||||
"version": "4.29.6",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { ConfirmDialog, ActionDialog } from 'vj/components/dialog';
|
||||
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import delay from 'vj/utils/delay';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
|
||||
import api, { gql } from 'vj/utils/api';
|
||||
|
||||
function update(name: string, uids: number[]) {
|
||||
return api(gql`
|
||||
domain { manage { group {
|
||||
update(name: ${name}, uids: ${uids})
|
||||
} } }
|
||||
`);
|
||||
}
|
||||
function del(name: string) {
|
||||
return api(gql`
|
||||
domain { manage { group {
|
||||
del(name: ${name})
|
||||
} } }
|
||||
`);
|
||||
}
|
||||
|
||||
const page = new NamedPage('domain_group', () => {
|
||||
const createGroupDialogContent = $(tpl`
|
||||
<div>
|
||||
<div class="row"><div class="columns">
|
||||
<h1>${i18n('Create Group')}</h1>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label>
|
||||
${i18n('Group Name')}
|
||||
<input name="create_group_name" type="text" class="textbox" data-autofocus>
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label>
|
||||
${i18n('Users')}
|
||||
<input name="create_group_users" type="text" class="textbox" autocomplete="off">
|
||||
</label>
|
||||
</div></div>
|
||||
</div>
|
||||
`);
|
||||
createGroupDialogContent.appendTo(document.body);
|
||||
const userSelect = UserSelectAutoComplete.getOrConstruct<UserSelectAutoComplete<true>>(
|
||||
createGroupDialogContent.find('[name="create_group_users"]'),
|
||||
{ multi: true },
|
||||
);
|
||||
$('input[data-gid]').get().forEach((ele) => {
|
||||
const input = UserSelectAutoComplete.getOrConstruct<UserSelectAutoComplete<true>>($(ele), { multi: true });
|
||||
const gid = ele.getAttribute('data-gid');
|
||||
let loaded = false;
|
||||
input.onChange(() => {
|
||||
if (input.value.length) {
|
||||
loaded = true;
|
||||
return;
|
||||
}
|
||||
if (!loaded) return;
|
||||
update(gid, input.value());
|
||||
});
|
||||
});
|
||||
|
||||
const createGroupDialog = new ActionDialog({
|
||||
$body: createGroupDialogContent,
|
||||
onDispatch(action) {
|
||||
const $name = createGroupDialog.$dom.find('[name="create_group_name"]');
|
||||
if (action === 'ok' && ($name.val() === '' || !userSelect.value())) {
|
||||
$name.focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
createGroupDialog.clear = function () {
|
||||
userSelect.clear();
|
||||
createGroupDialog.$dom.find('[name="create_group_name"]').val('');
|
||||
return this;
|
||||
};
|
||||
|
||||
function ensureAndGetSelectedGroups() {
|
||||
const groups = _.map(
|
||||
$('.domain-group tbody [type="checkbox"]:checked'),
|
||||
(ch) => $(ch).closest('tr').attr('data-gid'),
|
||||
);
|
||||
if (groups.length === 0) {
|
||||
Notification.error(i18n('Please select at least one group to perform this operation.'));
|
||||
return null;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
async function handleClickCreateGroup() {
|
||||
const action = await createGroupDialog.clear().open();
|
||||
if (action !== 'ok') return;
|
||||
const name = createGroupDialog.$dom.find('[name="create_group_name"]').val() as string;
|
||||
const uids = userSelect.value();
|
||||
try {
|
||||
await update(name, uids);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClickDeleteSelected() {
|
||||
const selectedGroups = ensureAndGetSelectedGroups();
|
||||
if (selectedGroups === null) return;
|
||||
const action = await new ConfirmDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n('Confirm deleting the selected groups?')}</p>
|
||||
<p>${i18n('Users with those roles will be removed from the domain.')}</p>
|
||||
</div>`,
|
||||
}).open();
|
||||
if (action !== 'yes') return;
|
||||
try {
|
||||
await Promise.all(selectedGroups.map((name) => del(name)));
|
||||
Notification.success(i18n('Selected groups have been deleted.'));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
$('[name="create_group"]').click(() => handleClickCreateGroup());
|
||||
$('[name="delete_selected"]').click(() => handleClickDeleteSelected());
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -25,6 +25,7 @@
|
||||
{{ sidemenu.render_item(null, 'domain_role') }}
|
||||
{{ sidemenu.render_item(null, 'domain_user') }}
|
||||
{{ sidemenu.render_item(null, 'domain_permission') }}
|
||||
{{ sidemenu.render_item(null, 'domain_group') }}
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "domain_base.html" %}
|
||||
{% block domain_content %}
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('{0}: Groups').format(domain.name) }}</h1>
|
||||
<div class="section__tools">
|
||||
<button class="primary rounded button" name="create_group">{{ _('Create Group') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ noscript_note.render() }}
|
||||
<div class="section__body no-padding domain-group">
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--checkbox">
|
||||
<col class="col--group">
|
||||
<col class="col--user">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" name="select_all" data-checkbox-toggle="group">
|
||||
</label>
|
||||
</th>
|
||||
<th class="col--group">{{ _('Group Name') }}</th>
|
||||
<th class="col--user">{{ _('Users') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for gdoc in groups -%}
|
||||
<tr data-gid="{{ gdoc.name }}">
|
||||
<td class="col--checkbox">
|
||||
<label class="compact checkbox">
|
||||
<input type="checkbox" data-checkbox-group="group" data-checkbox-range>
|
||||
</label>
|
||||
</td>
|
||||
<td class="col--group">{{ gdoc.name }}</td>
|
||||
<td class="col--user"><input type="text" value="{{ gdoc.uids.join(',') }}" data-gid="{{ gdoc.name }}"></input></td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section__body">
|
||||
<button class="rounded button" name="remove_selected">{{ _('Remove Selected Group') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,10 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function tpl(pieces, ...substitutions) {
|
||||
type Substitution = string | { templateRaw: true, html: string };
|
||||
|
||||
export default function tpl(pieces: TemplateStringsArray, ...substitutions: Substitution[]) {
|
||||
let result = pieces[0];
|
||||
for (let i = 0; i < substitutions.length; ++i) {
|
||||
const subst = substitutions[i];
|
||||
let substHtml;
|
||||
let substHtml: string;
|
||||
if (typeof subst === 'object' && subst.templateRaw) {
|
||||
substHtml = subst.html;
|
||||
} else substHtml = _.escape(String(subst));
|
||||
@@ -13,7 +15,7 @@ export default function tpl(pieces, ...substitutions) {
|
||||
return result;
|
||||
}
|
||||
|
||||
tpl.typoMsg = function (msg, raw = false) {
|
||||
tpl.typoMsg = function (msg: string, raw = false) {
|
||||
return tpl`
|
||||
<div class="typo">
|
||||
<p>${raw ? { html: msg, templateRaw: true } : msg}</p>
|
||||
@@ -21,7 +23,7 @@ tpl.typoMsg = function (msg, raw = false) {
|
||||
`;
|
||||
};
|
||||
|
||||
export function rawHtml(html) {
|
||||
export function rawHtml(html: string) {
|
||||
return {
|
||||
templateRaw: true,
|
||||
html,
|
||||
Reference in new issue
Block a user