/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useCallback, useMemo } from 'react';
import {
Button,
Collapsible,
Input,
Select,
Tag,
Typography,
Popconfirm,
} from '@douyinfe/semi-ui';
import {
IconPlus,
IconDelete,
IconChevronDown,
IconChevronUp,
} from '@douyinfe/semi-icons';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
let _idCounter = 0;
const uid = () => `gsu_${++_idCounter}`;
const OP_ADD = 'add';
const OP_REMOVE = 'remove';
const OP_APPEND = 'append';
function parsePrefix(rawKey) {
if (rawKey.startsWith('+:')) return { op: OP_ADD, groupName: rawKey.slice(2) };
if (rawKey.startsWith('-:')) return { op: OP_REMOVE, groupName: rawKey.slice(2) };
return { op: OP_APPEND, groupName: rawKey };
}
function toRawKey(op, groupName) {
if (op === OP_ADD) return `+:${groupName}`;
if (op === OP_REMOVE) return `-:${groupName}`;
return groupName;
}
function parseJSON(str) {
if (!str || !str.trim()) return {};
try { return JSON.parse(str); } catch { return {}; }
}
function flattenRules(nested) {
const rules = [];
for (const [userGroup, inner] of Object.entries(nested)) {
if (typeof inner !== 'object' || inner === null) continue;
for (const [rawKey, desc] of Object.entries(inner)) {
const { op, groupName } = parsePrefix(rawKey);
rules.push({
_id: uid(),
userGroup,
op,
targetGroup: groupName,
description: op === OP_REMOVE ? 'remove' : (typeof desc === 'string' ? desc : ''),
});
}
}
return rules;
}
function nestRules(rules) {
const result = {};
rules.forEach(({ userGroup, op, targetGroup, description }) => {
if (!userGroup || !targetGroup) return;
if (!result[userGroup]) result[userGroup] = {};
result[userGroup][toRawKey(op, targetGroup)] = description;
});
return result;
}
export function serializeGroupSpecialUsable(rules) {
const nested = nestRules(rules);
return Object.keys(nested).length === 0 ? '' : JSON.stringify(nested, null, 2);
}
const OP_TAG_MAP = {
[OP_ADD]: { color: 'green', label: '添加 (+:)' },
[OP_REMOVE]: { color: 'red', label: '移除 (-:)' },
[OP_APPEND]: { color: 'blue', label: '追加' },
};
function UsableGroupSection({ groupName, items, opOptions, onUpdate, onRemove, onAdd, t }) {
const [open, setOpen] = useState(false);
return (