mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
Merge branch 'master' into events
This commit is contained in:
20 files changed
+150
-39
No files matched your search
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
"license": "MIT",
|
||||
"main": "base.mjs",
|
||||
"dependencies": {
|
||||
"@eslint-react/eslint-plugin": "^1.51.2",
|
||||
"@eslint-react/eslint-plugin": "^1.51.3",
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@stylistic/eslint-plugin": "^4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||
"@typescript-eslint/parser": "^8.33.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-plugin-de-morgan": "^1.3.0",
|
||||
"eslint-plugin-github": "^6.0.0",
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@
|
||||
"mongodb-memory-server": "10.1.4",
|
||||
"nyc": "^17.1.0",
|
||||
"ora": "^8.2.0",
|
||||
"oxlint": "^0.16.12",
|
||||
"oxlint": "^1.0.0",
|
||||
"package-json": "^10.0.1",
|
||||
"semver": "^7.7.2",
|
||||
"simple-git": "^3.28.0",
|
||||
|
||||
@@ -117,7 +117,7 @@ function MonacoContainer({
|
||||
}
|
||||
|
||||
export default function ConfigEditor({
|
||||
schema, config, monaco, Markdown, onSave, registerAction, sidebar,
|
||||
schema, config, monaco, Markdown, onSave, registerAction, sidebar, dynamic,
|
||||
}) {
|
||||
const getValue = () => {
|
||||
try {
|
||||
@@ -178,7 +178,7 @@ export default function ConfigEditor({
|
||||
<div style={{
|
||||
overflowY: 'scroll', top: 0, bottom: 0, left: 0, right: 0, position: 'absolute', marginLeft: 10,
|
||||
}}>
|
||||
<Form schema={schema} initial={initial} value={value} onChange={updateFromForm} />
|
||||
<Form schema={schema} initial={initial} value={value} onChange={updateFromForm} dynamic={dynamic} />
|
||||
</div>
|
||||
</Allotment.Pane>
|
||||
{sidebar && <Allotment.Pane maxSize={220}>{sidebar}</Allotment.Pane>}
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
"version": "1.0.0-beta.3",
|
||||
"main": "./frontend/index.ts",
|
||||
"dependencies": {
|
||||
"allotment": "^1.20.3",
|
||||
"allotment": "^1.20.4",
|
||||
"diff": "^8.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"schemastery-react": "^0.1.3"
|
||||
"schemastery-react": "^0.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^8.0.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"sass": "^1.89.1"
|
||||
"sass": "^1.89.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
|
||||
@@ -434,7 +434,7 @@ export const DomainApi = {
|
||||
}),
|
||||
async (ctx, args) => {
|
||||
if (!ctx.user.hasPerm(PERM.PERM_EDIT_DOMAIN)) throw new PermissionError(PERM.PERM_EDIT_DOMAIN);
|
||||
if (args.uids) {
|
||||
if (args.uids?.length) {
|
||||
const res = await user.updateGroup(ctx.domain._id, args.name, args.uids);
|
||||
return res.upsertedCount > 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const versionNum = +process.version.replace(/v/gim, '').split('.')[0];
|
||||
if (versionNum < 18) throw new Error('NodeJS >=18 required');
|
||||
if (versionNum < 22) throw new Error('NodeJS >=22 required');
|
||||
|
||||
console.log('Process', process.pid, 'running as', process.env.NODE_APP_INSTANCE === '0' ? 'master' : 'worker');
|
||||
if (!global.Hydro) {
|
||||
|
||||
@@ -104,9 +104,9 @@ export async function set<K extends keyof DocType>(
|
||||
): Promise<DocType[K]> {
|
||||
await bus.parallel('document/set', domainId, docType, docId, $set, $unset);
|
||||
const update: UpdateFilter<DocType[K]> = {};
|
||||
if ($set) update.$set = $set;
|
||||
if ($unset) update.$unset = $unset;
|
||||
if ($push) update.$push = $push;
|
||||
if ($set && Object.keys($set).length) update.$set = $set;
|
||||
if ($unset && Object.keys($unset).length) update.$unset = $unset;
|
||||
if ($push && Object.keys($push).length) update.$push = $push;
|
||||
return await coll.findOneAndUpdate(
|
||||
{ domainId, docType, docId },
|
||||
update,
|
||||
@@ -317,14 +317,14 @@ export async function setMultiStatus<K extends keyof DocStatusType>(
|
||||
);
|
||||
}
|
||||
|
||||
export async function setStatusIfNotCondition<T extends keyof DocStatusType>(
|
||||
export async function setStatusIfCondition<T extends keyof DocStatusType>(
|
||||
domainId: string, docType: T, docId: DocStatusType[T]['docId'], uid: number,
|
||||
filter: Filter<DocStatusType[T]>, args: Partial<DocStatusType[T]> = {},
|
||||
returnDocument: 'before' | 'after' = 'after',
|
||||
): Promise<DocStatusType[T]> {
|
||||
try {
|
||||
return await collStatus.findOneAndUpdate(
|
||||
{ domainId, docType, docId, uid, $nor: [filter] },
|
||||
{ domainId, docType, docId, uid, ...filter },
|
||||
{ $set: args },
|
||||
{ upsert: true, returnDocument },
|
||||
);
|
||||
@@ -338,8 +338,8 @@ export async function setIfNotStatus<T extends keyof DocStatusType, K extends ke
|
||||
key: K, value: DocStatusType[T][K], ifNot: DocStatusType[T][K], args: Partial<DocStatusType[T]>,
|
||||
returnDocument: 'before' | 'after' = 'after',
|
||||
): Promise<DocStatusType[T]> {
|
||||
return await setStatusIfNotCondition(
|
||||
domainId, docType, docId, uid, { [key]: ifNot } as any,
|
||||
return await setStatusIfCondition(
|
||||
domainId, docType, docId, uid, { [key]: { $ne: ifNot } } as any,
|
||||
{ [key]: value, ...args }, returnDocument,
|
||||
);
|
||||
}
|
||||
@@ -474,7 +474,7 @@ global.Hydro.model.document = {
|
||||
revSetStatus,
|
||||
set,
|
||||
setIfNotStatus,
|
||||
setStatusIfNotCondition,
|
||||
setStatusIfCondition,
|
||||
setStatus,
|
||||
setMultiStatus,
|
||||
setSub,
|
||||
|
||||
@@ -231,14 +231,15 @@ export class ProblemModel {
|
||||
static async edit(domainId: string, _id: number, $set: Partial<ProblemDoc>): Promise<ProblemDoc> {
|
||||
const delpid = $set.pid === '';
|
||||
const ddoc = await DomainModel.get(domainId);
|
||||
const $unset = delpid ? { pid: '' } : {};
|
||||
if (delpid) {
|
||||
delete $set.pid;
|
||||
$set.sort = sortable(`P${_id}`, ddoc.namespaces);
|
||||
} else if ($set.pid) {
|
||||
$set.sort = sortable($set.pid, ddoc.namespaces);
|
||||
}
|
||||
await bus.parallel('problem/before-edit', $set);
|
||||
const result = await document.set(domainId, document.TYPE_PROBLEM, _id, $set, delpid ? { pid: '' } : undefined);
|
||||
await bus.parallel('problem/before-edit', $set, $unset);
|
||||
const result = await document.set(domainId, document.TYPE_PROBLEM, _id, $set, $unset);
|
||||
await bus.emit('problem/edit', result);
|
||||
return result;
|
||||
}
|
||||
@@ -419,10 +420,10 @@ export class ProblemModel {
|
||||
domainId: string, pid: number, uid: number,
|
||||
rid: ObjectId, status: number, score: number,
|
||||
) {
|
||||
const filter: Filter<ProblemStatusDoc> = { rid: { $ne: rid }, status: STATUS.STATUS_ACCEPTED };
|
||||
const res = await document.setStatusIfNotCondition(
|
||||
const condition = status === STATUS.STATUS_ACCEPTED ? {} : { status: { $ne: STATUS.STATUS_ACCEPTED } };
|
||||
const res = await document.setStatusIfCondition(
|
||||
domainId, document.TYPE_PROBLEM, pid, uid,
|
||||
filter, { rid, status, score },
|
||||
condition, { rid, status, score },
|
||||
);
|
||||
return !!res;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ArgMethod, buildProjection, Time } from '../utils';
|
||||
import { STATUS } from './builtin';
|
||||
import DomainModel from './domain';
|
||||
import problem from './problem';
|
||||
import * as SystemModel from './system';
|
||||
import task from './task';
|
||||
|
||||
export default class RecordModel {
|
||||
@@ -291,6 +292,14 @@ export async function apply(ctx: Context) {
|
||||
ctx.on('domain/delete', (domainId) => RecordModel.coll.deleteMany({ domainId }));
|
||||
ctx.on('record/judge', async (rdoc, updated) => {
|
||||
if (rdoc.status === STATUS.STATUS_ACCEPTED && updated) {
|
||||
if (SystemModel.get('record.statMode') === 'unique') {
|
||||
await RecordModel.collStat.deleteMany({
|
||||
_id: { $ne: rdoc._id },
|
||||
uid: rdoc.uid,
|
||||
pid: rdoc.pid,
|
||||
domainId: rdoc.domainId,
|
||||
});
|
||||
}
|
||||
await RecordModel.collStat.updateOne({
|
||||
_id: rdoc._id,
|
||||
}, {
|
||||
|
||||
@@ -334,6 +334,7 @@ SystemSetting(
|
||||
Setting('setting_basic', 'discussion.nodes', builtin.DEFAULT_NODES, 'yaml', 'discussion.nodes', 'Discussion Nodes'),
|
||||
Setting('setting_basic', 'problem.categories', builtin.CATEGORIES, 'yaml', 'problem.categories', 'Problem Categories'),
|
||||
Setting('setting_basic', 'training.enrolled-users', true, 'boolean', 'training.enrolled-users', 'Show enrolled users for training'),
|
||||
Setting('setting_basic', 'record.statMode', 'unique', 'text', 'record.statMode', 'Record stat mode'),
|
||||
Setting('setting_basic', 'pagination.problem', 100, 'number', 'pagination.problem', 'Problems per page'),
|
||||
Setting('setting_basic', 'pagination.contest', 20, 'number', 'pagination.contest', 'Contests per page'),
|
||||
Setting('setting_basic', 'pagination.discussion', 50, 'number', 'pagination.discussion', 'Discussions per page'),
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface EventMap {
|
||||
|
||||
'problem/before-add': (domainId: string, content: string, owner: number, docId: number, doc: Partial<ProblemDoc>) => VoidReturn
|
||||
'problem/add': (doc: Partial<ProblemDoc>, docId: number) => VoidReturn
|
||||
'problem/before-edit': (doc: Partial<ProblemDoc>) => VoidReturn
|
||||
'problem/before-edit': (doc: Partial<ProblemDoc>, $unset: OnlyFieldsOfType<ProblemDoc, any, true | '' | 1>) => VoidReturn
|
||||
'problem/edit': (doc: ProblemDoc) => VoidReturn
|
||||
'problem/before-del': (domainId: string, docId: number) => VoidReturn
|
||||
'problem/del': (domainId: string, docId: number) => VoidReturn
|
||||
|
||||
@@ -183,6 +183,7 @@ export function apply(ctx: Context) {
|
||||
about: Schema.string().role('markdown').default(defaultAbout),
|
||||
}),
|
||||
}));
|
||||
ctx.Route('config_schema', '/manage/config/schema.json', SystemConfigSchemaHandler, PRIV.PRIV_EDIT_SYSTEM);
|
||||
});
|
||||
if (process.env.HYDRO_CLI) return;
|
||||
ctx.Route('wiki_help', '/wiki/help', WikiHelpHandler);
|
||||
@@ -190,7 +191,6 @@ export function apply(ctx: Context) {
|
||||
ctx.Route('set_theme', '/set_theme/:theme', SetThemeHandler);
|
||||
ctx.Route('set_legacy', '/legacy', LegacyModeHandler);
|
||||
ctx.Route('markdown', '/markdown', MarkdownHandler);
|
||||
ctx.Route('config_schema', '/manage/config/schema.json', SystemConfigSchemaHandler, PRIV.PRIV_EDIT_SYSTEM);
|
||||
ctx.Route('media', '/media', RichMediaHandler);
|
||||
ctx.on('handler/after/DiscussionRaw', async (that) => {
|
||||
if (that.args.render && that.response.type === 'text/markdown') {
|
||||
|
||||
@@ -6,6 +6,7 @@ __langname: 简体中文
|
||||
'{0} is a moderator of this domain.': '{}是这个域的管理者之一。'
|
||||
'{0} is a superuser.': '{0}是超级管理员。'
|
||||
'{0} limit exceeded (limit: {2} operations in {1} seconds).': '{0} 超过频率限制。限制:{1} 秒内最多 {2} 次操作。'
|
||||
'{0} problem(s) selected': '{0} 道题目已选中'
|
||||
'{0} problems': '{0} 道题'
|
||||
'{0} sections': '{0} 小节'
|
||||
'{0} solutions': '{0} 条题解'
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@fontsource/source-code-pro": "^5.2.6",
|
||||
"@fontsource/ubuntu-mono": "^5.2.6",
|
||||
"@hydrooj/utils": "workspace:^",
|
||||
"@sentry/browser": "^9.27.0",
|
||||
"@sentry/browser": "^9.28.0",
|
||||
"@sentry/webpack-plugin": "^3.5.0",
|
||||
"@simplewebauthn/browser": "13.1.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@types/webpack-env": "^1.18.8",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"ajv": "^8.17.1",
|
||||
"allotment": "^1.20.3",
|
||||
"allotment": "^1.20.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"browser-update": "^3.3.55",
|
||||
"chalk": "^5.4.1",
|
||||
@@ -98,7 +98,7 @@
|
||||
"redux-promise-middleware": "^6.2.0",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"rupture": "^0.7.1",
|
||||
"sass": "^1.89.1",
|
||||
"sass": "^1.89.2",
|
||||
"sass-loader": "^16.0.5",
|
||||
"schemastery": "^3.16.1",
|
||||
"shorty.js": "^1.0.1",
|
||||
|
||||
+103
-6
@@ -1,6 +1,9 @@
|
||||
import parser, { SearchParserResult } from '@hydrooj/utils/lib/search';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import ProblemSelectAutoComplete from 'vj/components/autocomplete/components/ProblemSelectAutoComplete';
|
||||
import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete';
|
||||
import { ActionDialog, ConfirmDialog, Dialog } from 'vj/components/dialog';
|
||||
import Dropdown from 'vj/components/dropdown/Dropdown';
|
||||
@@ -18,6 +21,9 @@ const pinned: Record<string, string[]> = { category: [], difficulty: [] };
|
||||
const selections = { category: {}, difficulty: {} };
|
||||
const selectedTags: Record<string, string[]> = { category: [], difficulty: [] };
|
||||
|
||||
let selectedPids: string[] = [];
|
||||
let clearSelectionHandler: (() => void) | null = null;
|
||||
|
||||
function setDomSelected($dom, selected, icon?) {
|
||||
if (selected) {
|
||||
$dom.addClass('selected');
|
||||
@@ -163,15 +169,11 @@ function parseCategorySelection() {
|
||||
}
|
||||
|
||||
function ensureAndGetSelectedPids() {
|
||||
const pids = _.map(
|
||||
$('tbody [data-checkbox-group="problem"]:checked'),
|
||||
(ch) => $(ch).closest('tr').attr('data-pid'),
|
||||
);
|
||||
if (pids.length === 0) {
|
||||
if (selectedPids.length === 0) {
|
||||
Notification.error(i18n('Please select at least one problem to perform this operation.'));
|
||||
return null;
|
||||
}
|
||||
return pids;
|
||||
return selectedPids;
|
||||
}
|
||||
|
||||
async function handleOperation(operation) {
|
||||
@@ -204,6 +206,7 @@ async function handleOperation(operation) {
|
||||
await request.post('', { operation, pids, ...payload });
|
||||
Notification.success(i18n(`Selected problems have been ${operation === 'copy' ? 'copie' : operation}d.`));
|
||||
await delay(2000);
|
||||
clearSelectionHandler?.();
|
||||
loadQuery();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
@@ -282,6 +285,94 @@ function processElement(ele) {
|
||||
createHint('Hint::icon::difficulty', $(ele).find('th.col--difficulty'));
|
||||
}
|
||||
|
||||
function ProblemSelectionDisplay(props) { // eslint-disable-line
|
||||
const [pids, setPids] = React.useState<string[]>([]);
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
props.onClear?.(() => {
|
||||
setPids([]);
|
||||
});
|
||||
}, [props.onClear]);
|
||||
|
||||
React.useEffect(() => {
|
||||
$(document).on('click', '[data-checkbox-group="problem"]:checked', (ev) => {
|
||||
const $checkbox = $(ev.currentTarget);
|
||||
const pid = $checkbox.closest('tr').attr('data-pid');
|
||||
setPids((o) => Array.from(new Set([...o, pid])));
|
||||
});
|
||||
$(document).on('click', '[data-checkbox-group="problem"]:not(:checked)', (ev) => {
|
||||
const $checkbox = $(ev.currentTarget);
|
||||
const pid = $checkbox.closest('tr').attr('data-pid');
|
||||
setPids((o) => o.filter((i) => i !== pid));
|
||||
});
|
||||
$(document).on('click', '[data-checkbox-toggle="problem"]:checked', () => {
|
||||
const all = $('[data-checkbox-group="problem"]').map((_index, i) => $(i).closest('tr').attr('data-pid')).get();
|
||||
setPids((o) => Array.from(new Set([...o, ...all])));
|
||||
});
|
||||
$(document).on('click', '[data-checkbox-toggle="problem"]:not(:checked)', () => {
|
||||
const all = $('[data-checkbox-group="problem"]').map((_index, i) => $(i).closest('tr').attr('data-pid')).get();
|
||||
setPids((o) => o.filter((i) => !all.includes(i)));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateCheckboxSelection = React.useCallback(() => {
|
||||
for (const i of $('[data-checkbox-group="problem"]:checked')) {
|
||||
if (!pids.includes(i.closest('tr').dataset.pid)) {
|
||||
$(i).prop('checked', false);
|
||||
}
|
||||
}
|
||||
for (const i of pids) {
|
||||
$(`[data-pid="${i}"]`).find('[data-checkbox-group="problem"]')?.prop('checked', true);
|
||||
}
|
||||
}, [pids]);
|
||||
|
||||
React.useEffect(() => {
|
||||
updateCheckboxSelection();
|
||||
props.onChange(pids);
|
||||
$(document).on('vjContentNew', updateCheckboxSelection);
|
||||
return () => {
|
||||
$(document).off('vjContentNew', updateCheckboxSelection);
|
||||
};
|
||||
}, [pids]);
|
||||
|
||||
return (<>
|
||||
<a href="javascript:;" className="menu__link display-mode-hide" onClick={() => setDialogOpen(true)}>
|
||||
<span className="icon icon-stack"></span>
|
||||
{' '}{i18n('{0} problem(s) selected', pids.length)}
|
||||
</a>
|
||||
<div className="dialog withBg" style={{ display: dialogOpen ? 'flex' : 'none', zIndex: 1000, opacity: 1 }} onClick={() => setDialogOpen(false)}>
|
||||
<div className="dialog__content" style={{ transform: 'scale(1, 1)' }} onClick={(ev) => ev.stopPropagation()}>
|
||||
<div className="dialog__body" style={{ height: 'calc(100% - 45px)' }}>
|
||||
<div className="row">
|
||||
<div className="columns">
|
||||
<h1>Select Problems</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="columns">
|
||||
<ProblemSelectAutoComplete
|
||||
multi
|
||||
onChange={(v) => setPids(v.split(',').filter((i) => i.trim()))}
|
||||
selectedKeys={pids}
|
||||
/>
|
||||
<style>{'.autocomplete-wrapper { max-height: 50vh; }'}</style>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="columns clearfix">
|
||||
<div className="float-right dialog__action">
|
||||
<button className="rounded button" onClick={() => setPids([])}>{i18n('Clear')}</button>{' '}
|
||||
<button className="primary rounded button" onClick={() => setDialogOpen(false)}>{i18n('Ok')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>);
|
||||
}
|
||||
|
||||
const page = new NamedPage(['problem_main'], () => {
|
||||
const $body = $('body');
|
||||
$body.addClass('display-mode');
|
||||
@@ -344,6 +435,12 @@ const page = new NamedPage(['problem_main'], () => {
|
||||
$(document).on('vjContentNew', (e) => processElement(e.target));
|
||||
processElement(document);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('problem_selection'))
|
||||
.render(<ProblemSelectionDisplay
|
||||
onChange={(pids) => { selectedPids = pids; }}
|
||||
onClear={(handler) => { clearSelectionHandler = handler; }}
|
||||
/>);
|
||||
|
||||
addSpeculationRules({
|
||||
prerender: [{
|
||||
'where': {
|
||||
@@ -45,6 +45,7 @@ const page = new NamedPage('manage_config', async () => {
|
||||
});
|
||||
}}
|
||||
sidebar={<DomComponent childDom={$('.section.side').get(0)} />}
|
||||
dynamic={UiContext.dynamic || {}}
|
||||
/>
|
||||
<style>{`
|
||||
body {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<span class="icon icon-edit"></span>
|
||||
{{ _('Leave Edit Mode') }}
|
||||
</a>
|
||||
<div id="problem_selection" class="display-mode-hide"></div>
|
||||
{% if handler.user.hasPerm(perm.PERM_EDIT_PROBLEM) %}
|
||||
<a href="javascript:;" class="menu__link display-mode-hide" name="hide_selected_problems">
|
||||
<span class="icon icon-close"></span>
|
||||
|
||||
@@ -6,7 +6,7 @@ import DOMServer from 'react-dom/server';
|
||||
|
||||
export function substitute(str: string, obj: any) {
|
||||
return str.replace(/\{([^{}]+)\}/g, (match, key) => {
|
||||
if (obj[key] !== undefined) return obj[key].toString();
|
||||
if (obj[key] !== undefined && obj[key] !== null) return obj[key].toString();
|
||||
return `{${key}}`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user