mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implement frontend for schema versioning
Also includes major rework for anonymous user UI
This commit is contained in:
parent
abf11fecc0
commit
953a8d4700
|
@ -130,7 +130,7 @@ class LibraryItem(Model):
|
|||
|
||||
def versions(self) -> list['Version']:
|
||||
''' Get all Versions of this item. '''
|
||||
return list(Version.objects.filter(item=self.pk))
|
||||
return list(Version.objects.filter(item=self.pk).order_by('-time_create'))
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
|
@ -6,15 +6,17 @@ interface NavigationButtonProps {
|
|||
text?: string;
|
||||
icon: React.ReactNode;
|
||||
title?: string;
|
||||
titleHtml?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
function NavigationButton({ icon, title, onClick, text }: NavigationButtonProps) {
|
||||
function NavigationButton({ icon, title, titleHtml, onClick, text }: NavigationButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
tabIndex={-1}
|
||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
|
|
|
@ -5,7 +5,7 @@ function HelpRSFormMeta() {
|
|||
<h1>Паспорт схемы</h1>
|
||||
<p><b>Сохранить изменения</b>: Ctrl + S или кнопка Сохранить</p>
|
||||
<p><b>Владелец</b> обладает правом редактирования</p>
|
||||
<p><b>Общедоступные</b> схемы доступы для всех</p>
|
||||
<p><b>Общедоступные</b> схемы доступны для всех</p>
|
||||
<p><b>Неизменные</b> схемы редактируют только администраторы</p>
|
||||
<p><b>Клонировать</b> - создать копию схемы под своим именем</p>
|
||||
<p><b>Отслеживание</b> - схема в персональном списке</p>
|
||||
|
|
|
@ -50,7 +50,7 @@ function DescribeError({ error }: { error: ErrorData }) {
|
|||
|
||||
function InfoError({ error }: InfoErrorProps) {
|
||||
return (
|
||||
<AnimateFade className='px-3 py-2 min-w-[25rem] w-full text-sm font-semibold select-text clr-text-warning'>
|
||||
<AnimateFade className='px-3 py-2 min-w-[25rem] w-full text-sm font-semibold select-text clr-text-red'>
|
||||
<DescribeError error={error} />
|
||||
</AnimateFade>
|
||||
);
|
||||
|
|
44
rsconcept/frontend/src/components/VersionSelector.tsx
Normal file
44
rsconcept/frontend/src/components/VersionSelector.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { IVersionInfo } from '@/models/library';
|
||||
import { labelVersion } from '@/utils/labels';
|
||||
|
||||
import SelectSingle from './ui/SelectSingle';
|
||||
|
||||
interface VersionSelectorProps {
|
||||
items?: IVersionInfo[];
|
||||
value?: number;
|
||||
onSelectValue: (newValue?: number) => void;
|
||||
}
|
||||
|
||||
function VersionSelector({ items, value, onSelectValue }: VersionSelectorProps) {
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: undefined,
|
||||
label: labelVersion(undefined)
|
||||
},
|
||||
...(items?.map(version => ({
|
||||
value: version.id,
|
||||
label: version.version
|
||||
})) ?? [])
|
||||
];
|
||||
}, [items]);
|
||||
const valueLabel = useMemo(() => {
|
||||
const version = items?.find(ver => ver.id === value);
|
||||
return version ? version.version : labelVersion(undefined);
|
||||
}, [items, value]);
|
||||
|
||||
return (
|
||||
<SelectSingle
|
||||
className='w-full min-w-[12rem] text-ellipsis'
|
||||
options={options}
|
||||
value={{ value: value, label: valueLabel }}
|
||||
onChange={data => onSelectValue(data?.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default VersionSelector;
|
|
@ -7,6 +7,7 @@ import { CProps } from '../props';
|
|||
interface ButtonProps extends CProps.Control, CProps.Colors, CProps.Button {
|
||||
text?: string;
|
||||
icon?: React.ReactNode;
|
||||
titleHtml?: string;
|
||||
|
||||
dense?: boolean;
|
||||
hideTitle?: boolean;
|
||||
|
@ -17,6 +18,7 @@ function Button({
|
|||
text,
|
||||
icon,
|
||||
title,
|
||||
titleHtml,
|
||||
loading,
|
||||
dense,
|
||||
disabled,
|
||||
|
@ -46,7 +48,8 @@ function Button({
|
|||
className,
|
||||
colors
|
||||
)}
|
||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
data-tooltip-hidden={hideTitle}
|
||||
{...restProps}
|
||||
|
|
|
@ -8,13 +8,14 @@ import { CProps } from '../props';
|
|||
|
||||
export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'> {
|
||||
label?: string;
|
||||
titleHtml?: string;
|
||||
disabled?: boolean;
|
||||
|
||||
value: boolean;
|
||||
setValue?: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function Checkbox({ id, disabled, label, title, className, value, setValue, ...restProps }: CheckboxProps) {
|
||||
function Checkbox({ id, disabled, label, title, titleHtml, className, value, setValue, ...restProps }: CheckboxProps) {
|
||||
const cursor = useMemo(() => {
|
||||
if (disabled) {
|
||||
return 'cursor-not-allowed';
|
||||
|
@ -46,7 +47,8 @@ function Checkbox({ id, disabled, label, title, className, value, setValue, ...r
|
|||
)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
{...restProps}
|
||||
>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { CheckboxCheckedIcon, CheckboxNullIcon } from '../Icons';
|
|||
import { CheckboxProps } from './Checkbox';
|
||||
|
||||
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'setValue'> {
|
||||
titleHtml?: string;
|
||||
value: boolean | null;
|
||||
setValue?: (newValue: boolean | null) => void;
|
||||
}
|
||||
|
@ -16,6 +17,7 @@ function CheckboxTristate({
|
|||
disabled,
|
||||
label,
|
||||
title,
|
||||
titleHtml,
|
||||
className,
|
||||
value,
|
||||
setValue,
|
||||
|
@ -57,7 +59,8 @@ function CheckboxTristate({
|
|||
)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
{...restProps}
|
||||
>
|
||||
|
|
|
@ -8,12 +8,22 @@ import { CProps } from '../props';
|
|||
|
||||
interface DropdownButtonProps extends CProps.AnimatedButton {
|
||||
text?: string;
|
||||
titleHtml?: string;
|
||||
icon?: React.ReactNode;
|
||||
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function DropdownButton({ text, icon, className, title, onClick, children, ...restProps }: DropdownButtonProps) {
|
||||
function DropdownButton({
|
||||
text,
|
||||
icon,
|
||||
className,
|
||||
title,
|
||||
titleHtml,
|
||||
onClick,
|
||||
children,
|
||||
...restProps
|
||||
}: DropdownButtonProps) {
|
||||
return (
|
||||
<motion.button
|
||||
type='button'
|
||||
|
@ -30,7 +40,8 @@ function DropdownButton({ text, icon, className, title, onClick, children, ...re
|
|||
className
|
||||
)}
|
||||
variants={animateDropdownItem}
|
||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
{...restProps}
|
||||
>
|
||||
|
|
|
@ -6,11 +6,21 @@ import { CProps } from '../props';
|
|||
|
||||
interface MiniButtonProps extends CProps.Button {
|
||||
icon: React.ReactNode;
|
||||
titleHtml?: string;
|
||||
noHover?: boolean;
|
||||
hideTitle?: boolean;
|
||||
}
|
||||
|
||||
function MiniButton({ icon, noHover, hideTitle, tabIndex, title, className, ...restProps }: MiniButtonProps) {
|
||||
function MiniButton({
|
||||
icon,
|
||||
noHover,
|
||||
hideTitle,
|
||||
tabIndex,
|
||||
title,
|
||||
titleHtml,
|
||||
className,
|
||||
...restProps
|
||||
}: MiniButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
|
@ -26,7 +36,8 @@ function MiniButton({ icon, noHover, hideTitle, tabIndex, title, className, ...r
|
|||
},
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
data-tooltip-hidden={hideTitle}
|
||||
{...restProps}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { CProps } from '../props';
|
|||
|
||||
interface SelectorButtonProps extends CProps.Button {
|
||||
text?: string;
|
||||
titleHtml?: string;
|
||||
icon?: React.ReactNode;
|
||||
|
||||
colors?: string;
|
||||
|
@ -17,6 +18,7 @@ function SelectorButton({
|
|||
text,
|
||||
icon,
|
||||
title,
|
||||
titleHtml,
|
||||
colors = 'clr-btn-default',
|
||||
className,
|
||||
transparent,
|
||||
|
@ -38,7 +40,8 @@ function SelectorButton({
|
|||
className,
|
||||
!transparent && colors
|
||||
)}
|
||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
data-tooltip-hidden={hideTitle}
|
||||
{...restProps}
|
||||
|
|
|
@ -6,9 +6,10 @@ import { globalIDs } from '@/utils/constants';
|
|||
|
||||
interface TabLabelProps extends Omit<TabPropsImpl, 'children'> {
|
||||
label?: string;
|
||||
titleHtml?: string;
|
||||
}
|
||||
|
||||
function TabLabel({ label, title, className, ...otherProps }: TabLabelProps) {
|
||||
function TabLabel({ label, title, titleHtml, className, ...otherProps }: TabLabelProps) {
|
||||
return (
|
||||
<TabImpl
|
||||
className={clsx(
|
||||
|
@ -19,7 +20,8 @@ function TabLabel({ label, title, className, ...otherProps }: TabLabelProps) {
|
|||
'select-none hover:cursor-pointer',
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={title ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
{...otherProps}
|
||||
>
|
||||
|
|
|
@ -69,7 +69,7 @@ interface IRSFormContext {
|
|||
cstDelete: (data: IConstituentaList, callback?: () => void) => void;
|
||||
cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void;
|
||||
|
||||
versionCreate: (data: IVersionData, callback?: () => void) => void;
|
||||
versionCreate: (data: IVersionData, callback?: (version: number) => void) => void;
|
||||
versionUpdate: (target: number, data: IVersionData, callback?: () => void) => void;
|
||||
versionDelete: (target: number, callback?: () => void) => void;
|
||||
}
|
||||
|
@ -114,8 +114,8 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
const isArchive = useMemo(() => !!versionID, [versionID]);
|
||||
|
||||
const isClaimable = useMemo(() => {
|
||||
return (user?.id !== schema?.owner && schema?.is_common && !schema?.is_canonical) ?? false;
|
||||
}, [user, schema?.owner, schema?.is_common, schema?.is_canonical]);
|
||||
return isArchive && ((user?.id !== schema?.owner && schema?.is_common && !schema?.is_canonical) ?? false);
|
||||
}, [user, schema?.owner, schema?.is_common, schema?.is_canonical, isArchive]);
|
||||
|
||||
const isSubscribed = useMemo(() => {
|
||||
if (!user || !schema || !user.id) {
|
||||
|
@ -263,14 +263,14 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
const download = useCallback(
|
||||
(callback: DataCallback<Blob>) => {
|
||||
setError(undefined);
|
||||
getTRSFile(schemaID, {
|
||||
getTRSFile(schemaID, String(schema?.version) ?? '', {
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setError,
|
||||
onSuccess: callback
|
||||
});
|
||||
},
|
||||
[schemaID, setError]
|
||||
[schemaID, setError, schema]
|
||||
);
|
||||
|
||||
const cstCreate = useCallback(
|
||||
|
@ -382,7 +382,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
);
|
||||
|
||||
const versionCreate = useCallback(
|
||||
(data: IVersionData, callback?: () => void) => {
|
||||
(data: IVersionData, callback?: (version: number) => void) => {
|
||||
setError(undefined);
|
||||
postCreateVersion(schemaID, {
|
||||
data: data,
|
||||
|
@ -392,7 +392,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
|
|||
onSuccess: newData => {
|
||||
setSchema(newData.schema);
|
||||
library.localUpdateTimestamp(Number(schemaID));
|
||||
if (callback) callback();
|
||||
if (callback) callback(newData.version);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -123,7 +123,7 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
|
|||
{props.row.original.value ? (
|
||||
<MiniButton
|
||||
title='Очистить значение'
|
||||
icon={<BiX size='0.75rem' className='clr-text-warning' />}
|
||||
icon={<BiX size='0.75rem' className='clr-text-red' />}
|
||||
noHover
|
||||
onClick={() => handleClearArgument(props.row.original)}
|
||||
/>
|
||||
|
@ -175,9 +175,7 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
|
|||
<div className='flex'>
|
||||
<MiniButton
|
||||
title='Подставить значение аргумента'
|
||||
icon={
|
||||
<BiCheck size='1.25rem' className={!!argumentValue && !!selectedArgument ? 'clr-text-success' : ''} />
|
||||
}
|
||||
icon={<BiCheck size='1.25rem' className={!!argumentValue && !!selectedArgument ? 'clr-text-green' : ''} />}
|
||||
disabled={!argumentValue || !selectedArgument}
|
||||
onClick={() => handleAssignArgument(selectedArgument!, argumentValue)}
|
||||
/>
|
||||
|
@ -190,7 +188,7 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
|
|||
<MiniButton
|
||||
title='Очистить значение аргумента'
|
||||
disabled={!selectedClearable}
|
||||
icon={<BiX size='1.25rem' className={selectedClearable ? 'clr-text-warning' : ''} />}
|
||||
icon={<BiX size='1.25rem' className={selectedClearable ? 'clr-text-red' : ''} />}
|
||||
onClick={() => (selectedArgument ? handleClearArgument(selectedArgument) : undefined)}
|
||||
/>
|
||||
</div>
|
||||
|
|
61
rsconcept/frontend/src/dialogs/DlgCreateVersion.tsx
Normal file
61
rsconcept/frontend/src/dialogs/DlgCreateVersion.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { IVersionData, IVersionInfo } from '@/models/library';
|
||||
import { nextVersion } from '@/models/libraryAPI';
|
||||
import { classnames } from '@/utils/constants';
|
||||
|
||||
interface DlgCreateVersionProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
versions: IVersionInfo[];
|
||||
onCreate: (data: IVersionData) => void;
|
||||
}
|
||||
|
||||
function DlgCreateVersion({ hideWindow, versions, onCreate }: DlgCreateVersionProps) {
|
||||
const [version, setVersion] = useState(versions.length > 0 ? nextVersion(versions[0].version) : '1.0.0');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
const canSubmit = useMemo(() => {
|
||||
return !versions.find(ver => ver.version === version);
|
||||
}, [versions, version]);
|
||||
|
||||
function handleSubmit() {
|
||||
const data: IVersionData = {
|
||||
version: version,
|
||||
description: description
|
||||
};
|
||||
onCreate(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header='Создание версии'
|
||||
hideWindow={hideWindow}
|
||||
canSubmit={canSubmit}
|
||||
onSubmit={handleSubmit}
|
||||
submitText='Создать'
|
||||
className={clsx('w-[30rem]', 'py-2 px-6', classnames.flex_col)}
|
||||
>
|
||||
<TextInput
|
||||
dense
|
||||
label='Версия'
|
||||
className='w-[16rem]'
|
||||
value={version}
|
||||
onChange={event => setVersion(event.target.value)}
|
||||
/>
|
||||
<TextArea
|
||||
spellCheck
|
||||
label='Описание'
|
||||
rows={3}
|
||||
value={description}
|
||||
onChange={event => setDescription(event.target.value)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DlgCreateVersion;
|
|
@ -0,0 +1,114 @@
|
|||
'use client';
|
||||
|
||||
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { BiReset } from 'react-icons/bi';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { IVersionData, IVersionInfo } from '@/models/library';
|
||||
|
||||
import VersionsTable from './VersionsTable';
|
||||
|
||||
interface DlgEditVersionsProps {
|
||||
hideWindow: () => void;
|
||||
versions: IVersionInfo[];
|
||||
onDelete: (versionID: number) => void;
|
||||
onUpdate: (versionID: number, data: IVersionData) => void;
|
||||
}
|
||||
|
||||
function DlgEditVersions({ hideWindow, versions, onDelete, onUpdate }: DlgEditVersionsProps) {
|
||||
const { processing } = useRSForm();
|
||||
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined);
|
||||
|
||||
const [version, setVersion] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
if (!selected) {
|
||||
return false;
|
||||
}
|
||||
return versions.every(ver => ver.id === selected.id || ver.version != version);
|
||||
}, [selected, version, versions]);
|
||||
|
||||
const isModified = useMemo(() => {
|
||||
if (!selected) {
|
||||
return false;
|
||||
}
|
||||
return selected.version != version || selected.description != description;
|
||||
}, [version, description, selected]);
|
||||
|
||||
function handleUpdate() {
|
||||
if (!isModified || !selected || processing || !isValid) {
|
||||
return;
|
||||
}
|
||||
const data: IVersionData = {
|
||||
version: version,
|
||||
description: description
|
||||
};
|
||||
onUpdate(selected.id, data);
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
if (!selected) {
|
||||
return false;
|
||||
}
|
||||
setVersion(selected?.version ?? '');
|
||||
setDescription(selected?.description ?? '');
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setVersion(selected?.version ?? '');
|
||||
setDescription(selected?.description ?? '');
|
||||
}, [selected]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
readonly
|
||||
header='Редактирование версий'
|
||||
hideWindow={hideWindow}
|
||||
className='flex flex-col w-[40rem] px-6 gap-3 pb-6'
|
||||
>
|
||||
<VersionsTable
|
||||
processing={processing}
|
||||
items={versions}
|
||||
onDelete={onDelete}
|
||||
onSelect={versionID => setSelected(versions.find(ver => ver.id === versionID))}
|
||||
selected={selected?.id}
|
||||
/>
|
||||
<div className='flex'>
|
||||
<TextInput
|
||||
dense
|
||||
label='Версия'
|
||||
className='w-[16rem] mr-3'
|
||||
value={version}
|
||||
onChange={event => setVersion(event.target.value)}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Сохранить изменения'
|
||||
disabled={!isModified || !isValid || processing}
|
||||
icon={<FiSave size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleUpdate}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Сбросить несохраненные изменения'
|
||||
disabled={!isModified}
|
||||
onClick={handleReset}
|
||||
icon={<BiReset size='1.25rem' className='icon-primary' />}
|
||||
/>
|
||||
</div>
|
||||
<TextArea
|
||||
spellCheck
|
||||
label='Описание'
|
||||
rows={3}
|
||||
value={description}
|
||||
onChange={event => setDescription(event.target.value)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DlgEditVersions;
|
102
rsconcept/frontend/src/dialogs/DlgEditVersions/VersionsTable.tsx
Normal file
102
rsconcept/frontend/src/dialogs/DlgEditVersions/VersionsTable.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import { BiX } from 'react-icons/bi';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/DataTable';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { IVersionInfo } from '@/models/library';
|
||||
|
||||
interface VersionsTableProps {
|
||||
processing: boolean;
|
||||
items: IVersionInfo[];
|
||||
selected?: number;
|
||||
onDelete: (versionID: number) => void;
|
||||
onSelect: (versionID: number) => void;
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<IVersionInfo>();
|
||||
|
||||
function VersionsTable({ processing, items, onDelete, selected, onSelect }: VersionsTableProps) {
|
||||
const intl = useIntl();
|
||||
const { colors } = useConceptTheme();
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor('version', {
|
||||
id: 'version',
|
||||
header: 'Версия',
|
||||
cell: props => <div className='min-w-[6rem] max-w-[6rem] text-ellipsis'>{props.getValue()}</div>
|
||||
}),
|
||||
columnHelper.accessor('description', {
|
||||
id: 'description',
|
||||
header: 'Описание',
|
||||
size: 800,
|
||||
minSize: 800,
|
||||
maxSize: 800,
|
||||
cell: props => <div className='text-ellipsis'>{props.getValue()}</div>
|
||||
}),
|
||||
columnHelper.accessor('time_create', {
|
||||
id: 'time_create',
|
||||
header: 'Дата создания',
|
||||
cell: props => (
|
||||
<div className='whitespace-nowrap'>
|
||||
{new Date(props.getValue()).toLocaleString(intl.locale, {
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
size: 50,
|
||||
minSize: 50,
|
||||
maxSize: 50,
|
||||
cell: props => (
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Удалить версию'
|
||||
disabled={processing}
|
||||
icon={<BiX size='1rem' className='icon-red' />}
|
||||
onClick={() => onDelete(props.row.original.id)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
],
|
||||
[onDelete, intl, processing]
|
||||
);
|
||||
|
||||
const conditionalRowStyles = useMemo(
|
||||
(): IConditionalStyle<IVersionInfo>[] => [
|
||||
{
|
||||
when: (version: IVersionInfo) => version.id === selected,
|
||||
style: {
|
||||
backgroundColor: colors.bgSelected
|
||||
}
|
||||
}
|
||||
],
|
||||
[selected, colors]
|
||||
);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
dense
|
||||
noFooter
|
||||
className={clsx('mb-2', 'max-h-[17.4rem] min-h-[17.4rem]', 'border', 'overflow-y-auto')}
|
||||
data={items}
|
||||
columns={columns}
|
||||
headPosition='0'
|
||||
onRowClicked={rowData => onSelect(rowData.id)}
|
||||
conditionalRowStyles={conditionalRowStyles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default VersionsTable;
|
1
rsconcept/frontend/src/dialogs/DlgEditVersions/index.tsx
Normal file
1
rsconcept/frontend/src/dialogs/DlgEditVersions/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './DlgEditVersions';
|
|
@ -182,7 +182,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
|||
<MiniButton
|
||||
noHover
|
||||
title='Внести словоформу'
|
||||
icon={<BiCheck size='1.25rem' className={inputText && inputGrams.length !== 0 ? 'clr-text-success' : ''} />}
|
||||
icon={<BiCheck size='1.25rem' className={inputText && inputGrams.length !== 0 ? 'clr-text-green' : ''} />}
|
||||
disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
|
||||
onClick={handleAddForm}
|
||||
/>
|
||||
|
@ -200,7 +200,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
|||
<MiniButton
|
||||
noHover
|
||||
title='Сбросить все словоформы'
|
||||
icon={<BiX size='1rem' className={forms.length !== 0 ? 'clr-text-warning' : ''} />}
|
||||
icon={<BiX size='1rem' className={forms.length !== 0 ? 'clr-text-red' : ''} />}
|
||||
disabled={textProcessor.loading || forms.length === 0}
|
||||
onClick={handleResetAll}
|
||||
/>
|
||||
|
|
|
@ -4,8 +4,8 @@ import clsx from 'clsx';
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { BiX } from 'react-icons/bi';
|
||||
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import DataTable, { createColumnHelper } from '@/components/DataTable';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import WordFormBadge from '@/components/WordFormBadge';
|
||||
import { IWordForm } from '@/models/language';
|
||||
|
||||
|
@ -60,7 +60,7 @@ function WordFormsTable({ forms, setForms, onFormSelect }: WordFormsTableProps)
|
|||
<MiniButton
|
||||
noHover
|
||||
title='Удалить словоформу'
|
||||
icon={<BiX size='1rem' className='clr-text-warning' />}
|
||||
icon={<BiX size='1rem' className='icon-red' />}
|
||||
onClick={() => handleDeleteRow(props.row.index)}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -27,3 +27,18 @@ export function cloneTitle(target: ILibraryItem): string {
|
|||
return target.title + '+';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate next version for {@link IVersionInfo}.
|
||||
*/
|
||||
export function nextVersion(version: string): string {
|
||||
const dot = version.lastIndexOf('.');
|
||||
if (!dot) {
|
||||
return version;
|
||||
}
|
||||
const lastNumber = Number(version.substring(dot + 1));
|
||||
if (!lastNumber) {
|
||||
return version;
|
||||
}
|
||||
return `${version.substring(0, dot)}.${lastNumber + 1}`;
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
|
|||
size: 400,
|
||||
minSize: 100,
|
||||
maxSize: 400,
|
||||
cell: props => getUserLabel(props.cell.getValue()),
|
||||
cell: props => getUserLabel(props.getValue()),
|
||||
enableSorting: true,
|
||||
sortingFn: 'text'
|
||||
}),
|
||||
|
@ -87,7 +87,7 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
|
|||
header: windowSize.isSmall ? 'Дата' : 'Обновлена',
|
||||
cell: props => (
|
||||
<div className='whitespace-nowrap'>
|
||||
{new Date(props.cell.getValue()).toLocaleString(intl.locale, {
|
||||
{new Date(props.getValue()).toLocaleString(intl.locale, {
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
|
|
|
@ -19,7 +19,7 @@ import { classnames, resources } from '@/utils/constants';
|
|||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
|
||||
return (
|
||||
<div className='text-sm select-text clr-text-warning'>
|
||||
<div className='text-sm select-text clr-text-red'>
|
||||
На Портале отсутствует такое сочетание имени пользователя и пароля
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,7 +16,7 @@ import { classnames } from '@/utils/constants';
|
|||
|
||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
|
||||
return <div className='mt-6 text-sm select-text clr-text-warning'>Данная ссылка не действительна</div>;
|
||||
return <div className='mt-6 text-sm select-text clr-text-red'>Данная ссылка не действительна</div>;
|
||||
} else {
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
|
|
|
@ -4,10 +4,8 @@ import { useMemo } from 'react';
|
|||
import { BiDownvote, BiDuplicate, BiPlusCircle, BiReset, BiTrash, BiUpvote } from 'react-icons/bi';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
|
||||
import HelpButton from '@/components/Help/HelpButton';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
interface ConstituentaToolbarProps {
|
||||
isMutable: boolean;
|
||||
|
@ -40,46 +38,45 @@ function ConstituentaToolbar({
|
|||
<MiniButton
|
||||
title='Сохранить изменения [Ctrl + S]'
|
||||
disabled={!canSave}
|
||||
icon={<FiSave size='1.25rem' className={canSave ? 'clr-text-primary' : ''} />}
|
||||
icon={<FiSave size='1.25rem' className='icon-primary' />}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Сбросить несохраненные изменения'
|
||||
disabled={!canSave}
|
||||
onClick={onReset}
|
||||
icon={<BiReset size='1.25rem' className={canSave ? 'clr-text-primary' : ''} />}
|
||||
icon={<BiReset size='1.25rem' className='icon-primary' />}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Создать конституенту после данной'
|
||||
disabled={!isMutable}
|
||||
onClick={onCreate}
|
||||
icon={<BiPlusCircle size={'1.25rem'} className={isMutable ? 'clr-text-success' : ''} />}
|
||||
icon={<BiPlusCircle size={'1.25rem'} className='icon-green' />}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Клонировать конституенту [Alt + V]'
|
||||
disabled={!isMutable}
|
||||
onClick={onClone}
|
||||
icon={<BiDuplicate size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
|
||||
icon={<BiDuplicate size='1.25rem' className='icon-green' />}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Удалить редактируемую конституенту'
|
||||
disabled={!isMutable}
|
||||
onClick={onDelete}
|
||||
icon={<BiTrash size='1.25rem' className={isMutable ? 'clr-text-warning' : ''} />}
|
||||
icon={<BiTrash size='1.25rem' className='icon-red' />}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить вверх [Alt + вверх]'
|
||||
icon={<BiUpvote size='1.25rem' className={isMutable ? 'clr-text-primary' : ''} />}
|
||||
icon={<BiUpvote size='1.25rem' className='icon-primary' />}
|
||||
disabled={!isMutable}
|
||||
onClick={onMoveUp}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Переместить вниз [Alt + вниз]'
|
||||
icon={<BiDownvote size='1.25rem' className={isMutable ? 'clr-text-primary' : ''} />}
|
||||
icon={<BiDownvote size='1.25rem' className='icon-primary' />}
|
||||
disabled={!isMutable}
|
||||
onClick={onMoveDown}
|
||||
/>
|
||||
<HelpButton topic={HelpTopic.CONSTITUENTA} offset={4} />
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import clsx from 'clsx';
|
||||
import { LiaEdit } from 'react-icons/lia';
|
||||
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
|
@ -5,34 +6,42 @@ import Overlay from '@/components/ui/Overlay';
|
|||
import { IConstituenta } from '@/models/rsform';
|
||||
|
||||
interface ControlsOverlayProps {
|
||||
disabled?: boolean;
|
||||
constituenta?: IConstituenta;
|
||||
isMutable?: boolean;
|
||||
|
||||
onRename: () => void;
|
||||
onEditTerm: () => void;
|
||||
}
|
||||
|
||||
function ControlsOverlay({ disabled, constituenta, onRename, onEditTerm }: ControlsOverlayProps) {
|
||||
function ControlsOverlay({ constituenta, isMutable, onRename, onEditTerm }: ControlsOverlayProps) {
|
||||
return (
|
||||
<Overlay position='top-1 left-[4.1rem]' className='flex select-none'>
|
||||
<MiniButton
|
||||
title={`Редактировать словоформы термина: ${constituenta?.term_forms.length ?? 0}`}
|
||||
disabled={disabled}
|
||||
noHover
|
||||
onClick={onEditTerm}
|
||||
icon={<LiaEdit size='1rem' className={!disabled ? 'clr-text-primary' : ''} />}
|
||||
/>
|
||||
<div className='pt-1 pl-[1.375rem] text-sm font-medium whitespace-nowrap'>
|
||||
{isMutable ? (
|
||||
<MiniButton
|
||||
title={`Редактировать словоформы термина: ${constituenta?.term_forms.length ?? 0}`}
|
||||
noHover
|
||||
onClick={onEditTerm}
|
||||
icon={<LiaEdit size='1rem' className='icon-primary' />}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={clsx(
|
||||
'pt-1 pl-[1.375rem]', // prettier: split lines
|
||||
'text-sm font-medium whitespace-nowrap',
|
||||
!isMutable && 'pl-[2.8rem]'
|
||||
)}
|
||||
>
|
||||
<span>Имя </span>
|
||||
<span className='ml-1'>{constituenta?.alias ?? ''}</span>
|
||||
</div>
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Переименовать конституенту'
|
||||
disabled={disabled}
|
||||
onClick={onRename}
|
||||
icon={<LiaEdit size='1rem' className={!disabled ? 'clr-text-primary' : ''} />}
|
||||
/>
|
||||
{isMutable ? (
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Переименовать конституенту'
|
||||
onClick={onRename}
|
||||
icon={<LiaEdit size='1rem' className='icon-primary' />}
|
||||
/>
|
||||
) : null}
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,10 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
|||
const [showList, setShowList] = useLocalStorage('rseditor-show-list', true);
|
||||
const [toggleReset, setToggleReset] = useState(false);
|
||||
|
||||
const disabled = useMemo(() => !activeCst || !controller.isMutable, [activeCst, controller.isMutable]);
|
||||
const disabled = useMemo(
|
||||
() => !activeCst || !controller.isContentEditable,
|
||||
[activeCst, controller.isContentEditable]
|
||||
);
|
||||
|
||||
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (disabled) {
|
||||
|
@ -73,20 +76,22 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
|||
|
||||
return (
|
||||
<>
|
||||
<ConstituentaToolbar
|
||||
isMutable={!disabled}
|
||||
isModified={isModified}
|
||||
onMoveUp={controller.moveUp}
|
||||
onMoveDown={controller.moveDown}
|
||||
onSubmit={initiateSubmit}
|
||||
onReset={() => setToggleReset(prev => !prev)}
|
||||
onDelete={controller.deleteCst}
|
||||
onClone={controller.cloneCst}
|
||||
onCreate={() => controller.createCst(activeCst?.cst_type, false)}
|
||||
/>
|
||||
{controller.isContentEditable ? (
|
||||
<ConstituentaToolbar
|
||||
isMutable={!disabled}
|
||||
isModified={isModified}
|
||||
onMoveUp={controller.moveUp}
|
||||
onMoveDown={controller.moveDown}
|
||||
onSubmit={initiateSubmit}
|
||||
onReset={() => setToggleReset(prev => !prev)}
|
||||
onDelete={controller.deleteCst}
|
||||
onClone={controller.cloneCst}
|
||||
onCreate={() => controller.createCst(activeCst?.cst_type, false)}
|
||||
/>
|
||||
) : null}
|
||||
<div tabIndex={-1} className='flex max-w-[95rem]' onKeyDown={handleInput}>
|
||||
<FormConstituenta
|
||||
disabled={disabled}
|
||||
isMutable={!disabled}
|
||||
showList={showList}
|
||||
id={globalIDs.constituenta_editor}
|
||||
constituenta={activeCst}
|
||||
|
|
|
@ -22,7 +22,7 @@ import ControlsOverlay from './ControlsOverlay';
|
|||
export const ROW_SIZE_IN_CHARACTERS = 70;
|
||||
|
||||
interface FormConstituentaProps {
|
||||
disabled?: boolean;
|
||||
isMutable?: boolean;
|
||||
showList: boolean;
|
||||
|
||||
id?: string;
|
||||
|
@ -38,7 +38,7 @@ interface FormConstituentaProps {
|
|||
}
|
||||
|
||||
function FormConstituenta({
|
||||
disabled,
|
||||
isMutable,
|
||||
showList,
|
||||
id,
|
||||
isModified,
|
||||
|
@ -114,7 +114,7 @@ function FormConstituenta({
|
|||
|
||||
return (
|
||||
<>
|
||||
<ControlsOverlay disabled={disabled} constituenta={constituenta} onEditTerm={onEditTerm} onRename={onRename} />
|
||||
<ControlsOverlay isMutable={isMutable} constituenta={constituenta} onEditTerm={onEditTerm} onRename={onRename} />
|
||||
<form
|
||||
id={id}
|
||||
className={clsx('mt-1 w-full md:w-[47.8rem] shrink-0', 'px-4 py-1', classnames.flex_col)}
|
||||
|
@ -127,7 +127,7 @@ function FormConstituenta({
|
|||
value={term}
|
||||
initialValue={constituenta?.term_raw ?? ''}
|
||||
resolved={constituenta?.term_resolved ?? ''}
|
||||
disabled={disabled}
|
||||
disabled={!isMutable}
|
||||
onChange={newValue => setTerm(newValue)}
|
||||
/>
|
||||
<TextArea
|
||||
|
@ -148,7 +148,7 @@ function FormConstituenta({
|
|||
value={expression}
|
||||
activeCst={constituenta}
|
||||
showList={showList}
|
||||
disabled={disabled}
|
||||
disabled={!isMutable}
|
||||
toggleReset={toggleReset}
|
||||
onToggleList={onToggleList}
|
||||
onChange={newValue => setExpression(newValue)}
|
||||
|
@ -162,7 +162,7 @@ function FormConstituenta({
|
|||
value={textDefinition}
|
||||
initialValue={constituenta?.definition_raw ?? ''}
|
||||
resolved={constituenta?.definition_resolved ?? ''}
|
||||
disabled={disabled}
|
||||
disabled={!isMutable}
|
||||
onChange={newValue => setTextDefinition(newValue)}
|
||||
/>
|
||||
<TextArea
|
||||
|
@ -170,16 +170,18 @@ function FormConstituenta({
|
|||
label='Конвенция / Комментарий'
|
||||
placeholder='Договоренность об интерпретации или пояснение'
|
||||
value={convention}
|
||||
disabled={disabled}
|
||||
disabled={!isMutable}
|
||||
rows={convention.length > ROW_SIZE_IN_CHARACTERS ? 3 : 2}
|
||||
onChange={event => setConvention(event.target.value)}
|
||||
/>
|
||||
<SubmitButton
|
||||
text='Сохранить изменения'
|
||||
className='self-center'
|
||||
disabled={!isModified || disabled}
|
||||
icon={<FiSave size='1.25rem' />}
|
||||
/>
|
||||
{isMutable ? (
|
||||
<SubmitButton
|
||||
text='Сохранить изменения'
|
||||
className='self-center'
|
||||
disabled={!isModified || !isMutable}
|
||||
icon={<FiSave size='1.25rem' />}
|
||||
/>
|
||||
) : null}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { FaRegKeyboard } from 'react-icons/fa6';
|
|||
import { RiNodeTree } from 'react-icons/ri';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import HelpButton from '@/components/Help/HelpButton';
|
||||
import RSInput from '@/components/RSInput';
|
||||
import { RSTextWrapper } from '@/components/RSInput/textEditing';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
|
@ -16,6 +17,7 @@ import { useRSForm } from '@/context/RSFormContext';
|
|||
import DlgShowAST from '@/dialogs/DlgShowAST';
|
||||
import useCheckExpression from '@/hooks/useCheckExpression';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
import { getDefinitionPrefix } from '@/models/rsformAPI';
|
||||
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang';
|
||||
|
@ -152,7 +154,7 @@ function EditorRSExpression({
|
|||
</AnimatePresence>
|
||||
|
||||
<div>
|
||||
<Overlay position='top-0 right-0 flex'>
|
||||
<Overlay position='top-[-0.5rem] right-0 flex'>
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Отображение специальной клавиатуры'
|
||||
|
@ -173,7 +175,7 @@ function EditorRSExpression({
|
|||
/>
|
||||
</Overlay>
|
||||
|
||||
<Overlay position='top-[-0.5rem] pl-[6rem] sm:pl-0 right-1/2 translate-x-1/2'>
|
||||
<Overlay position='top-[-0.5rem] pl-[8rem] sm:pl-[4rem] right-1/2 translate-x-1/2 flex'>
|
||||
<StatusBar
|
||||
processing={loading}
|
||||
isModified={isModified}
|
||||
|
@ -181,6 +183,7 @@ function EditorRSExpression({
|
|||
parseData={parseData}
|
||||
onAnalyze={() => handleCheckExpression()}
|
||||
/>
|
||||
<HelpButton topic={HelpTopic.CONSTITUENTA} offset={4} />
|
||||
</Overlay>
|
||||
|
||||
<RSInput
|
||||
|
|
|
@ -33,7 +33,7 @@ function ParsingResult({ isOpen, data, disabled, onShowError }: ParsingResultPro
|
|||
return (
|
||||
<p
|
||||
key={`error-${index}`}
|
||||
className={`clr-text-warning ${disabled ? '' : 'cursor-pointer'}`}
|
||||
className={`clr-text-red ${disabled ? '' : 'cursor-pointer'}`}
|
||||
onClick={disabled ? undefined : () => onShowError(error)}
|
||||
>
|
||||
<span className='mr-1 font-semibold underline'>
|
||||
|
|
|
@ -9,7 +9,6 @@ import { useAuth } from '@/context/AuthContext';
|
|||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
import FormRSForm from './FormRSForm';
|
||||
import RSFormStats from './RSFormStats';
|
||||
import RSFormToolbar from './RSFormToolbar';
|
||||
|
@ -21,7 +20,6 @@ interface EditorRSFormProps {
|
|||
}
|
||||
|
||||
function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProps) {
|
||||
const { isMutable } = useRSEdit();
|
||||
const { schema, isClaimable, isSubscribed, processing } = useRSForm();
|
||||
const { user } = useAuth();
|
||||
|
||||
|
@ -54,12 +52,7 @@ function EditorRSForm({ isModified, onDestroy, setIsModified }: EditorRSFormProp
|
|||
/>
|
||||
<div tabIndex={-1} onKeyDown={handleInput} className={clsx('flex flex-col sm:flex-row', 'sm:w-fit w-full')}>
|
||||
<FlexColumn className='px-4 pb-2'>
|
||||
<FormRSForm
|
||||
disabled={!isMutable}
|
||||
id={globalIDs.library_item_editor}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
/>
|
||||
<FormRSForm id={globalIDs.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
||||
|
||||
<Divider margins='my-1' />
|
||||
|
||||
|
|
|
@ -3,28 +3,35 @@
|
|||
import clsx from 'clsx';
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
import { LuGitBranchPlus, LuPencilLine } from 'react-icons/lu';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import Label from '@/components/ui/Label';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import VersionSelector from '@/components/VersionSelector';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { LibraryItemType } from '@/models/library';
|
||||
import { IRSFormCreateData } from '@/models/rsform';
|
||||
import { classnames, limits, patterns } from '@/utils/constants';
|
||||
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface FormRSFormProps {
|
||||
id?: string;
|
||||
disabled: boolean;
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
function FormRSForm({ id, disabled, isModified, setIsModified }: FormRSFormProps) {
|
||||
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||
const { schema, update, processing } = useRSForm();
|
||||
const { user } = useAuth();
|
||||
const controller = useRSEdit();
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [alias, setAlias] = useState('');
|
||||
|
@ -95,49 +102,79 @@ function FormRSForm({ id, disabled, isModified, setIsModified }: FormRSFormProps
|
|||
required
|
||||
label='Полное название'
|
||||
value={title}
|
||||
disabled={disabled}
|
||||
disabled={!controller.isContentEditable}
|
||||
onChange={event => setTitle(event.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label='Сокращение'
|
||||
className='w-[14rem]'
|
||||
pattern={patterns.library_alias}
|
||||
title={`не более ${limits.library_alias_len} символов`}
|
||||
disabled={disabled}
|
||||
value={alias}
|
||||
onChange={event => setAlias(event.target.value)}
|
||||
/>
|
||||
<div className='flex justify-between w-full gap-3'>
|
||||
<TextInput
|
||||
required
|
||||
label='Сокращение'
|
||||
className='w-[14rem]'
|
||||
pattern={patterns.library_alias}
|
||||
title={`не более ${limits.library_alias_len} символов`}
|
||||
disabled={!controller.isContentEditable}
|
||||
value={alias}
|
||||
onChange={event => setAlias(event.target.value)}
|
||||
/>
|
||||
<div className='flex flex-col'>
|
||||
{controller.isMutable ? (
|
||||
<Overlay position='top-[-0.25rem] right-[-0.25rem] flex'>
|
||||
<MiniButton
|
||||
noHover
|
||||
title={controller.isContentEditable ? 'Создать версию' : 'Переключитесь на актуальную версию'}
|
||||
disabled={!controller.isContentEditable}
|
||||
onClick={controller.createVersion}
|
||||
icon={<LuGitBranchPlus size='1.25rem' className='icon-green' />}
|
||||
/>
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Редактировать версии'
|
||||
disabled={!schema || schema?.versions.length === 0}
|
||||
onClick={controller.editVersions}
|
||||
icon={<LuPencilLine size='1.25rem' className='icon-primary' />}
|
||||
/>
|
||||
</Overlay>
|
||||
) : null}
|
||||
<Label text='Версия' className='mb-2' />
|
||||
<VersionSelector
|
||||
value={schema?.version} // prettier: split lines
|
||||
items={schema?.versions}
|
||||
onSelectValue={controller.viewVersion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TextArea
|
||||
label='Комментарий'
|
||||
rows={3}
|
||||
value={comment}
|
||||
disabled={disabled}
|
||||
disabled={!controller.isContentEditable}
|
||||
onChange={event => setComment(event.target.value)}
|
||||
/>
|
||||
<div className='flex justify-between whitespace-nowrap'>
|
||||
<Checkbox
|
||||
label='Общедоступная схема'
|
||||
title='Общедоступные схемы видны всем пользователям и могут быть изменены'
|
||||
disabled={disabled}
|
||||
disabled={!controller.isContentEditable}
|
||||
value={common}
|
||||
setValue={value => setCommon(value)}
|
||||
/>
|
||||
<Checkbox
|
||||
label='Неизменная схема'
|
||||
title='Только администраторы могут присваивать схемам неизменный статус'
|
||||
disabled={disabled || !user?.is_staff}
|
||||
disabled={!controller.isContentEditable || !user?.is_staff}
|
||||
value={canonical}
|
||||
setValue={value => setCanonical(value)}
|
||||
/>
|
||||
</div>
|
||||
<SubmitButton
|
||||
text='Сохранить изменения'
|
||||
className='self-center'
|
||||
loading={processing}
|
||||
disabled={!isModified || disabled}
|
||||
icon={<FiSave size='1.25rem' />}
|
||||
/>
|
||||
{controller.isContentEditable ? (
|
||||
<SubmitButton
|
||||
text='Сохранить изменения'
|
||||
className='self-center'
|
||||
loading={processing}
|
||||
disabled={!isModified}
|
||||
icon={<FiSave size='1.25rem' />}
|
||||
/>
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,47 +35,54 @@ function RSFormToolbar({
|
|||
const canSave = useMemo(() => modified && controller.isMutable, [modified, controller.isMutable]);
|
||||
return (
|
||||
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
|
||||
<MiniButton
|
||||
title='Сохранить изменения [Ctrl + S]'
|
||||
disabled={!canSave}
|
||||
icon={<FiSave size='1.25rem' className={canSave ? 'clr-text-primary' : ''} />}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
{controller.isContentEditable ? (
|
||||
<MiniButton
|
||||
title='Сохранить изменения [Ctrl + S]'
|
||||
disabled={!canSave}
|
||||
icon={<FiSave size='1.25rem' className='icon-primary' />}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
) : null}
|
||||
<MiniButton
|
||||
title='Поделиться схемой'
|
||||
icon={<BiShareAlt size='1.25rem' className='clr-text-primary' />}
|
||||
icon={<BiShareAlt size='1.25rem' className='icon-primary' />}
|
||||
onClick={controller.share}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Скачать TRS файл'
|
||||
icon={<BiDownload size='1.25rem' className='clr-text-primary' />}
|
||||
icon={<BiDownload size='1.25rem' className='icon-primary' />}
|
||||
onClick={controller.download}
|
||||
/>
|
||||
<MiniButton
|
||||
title={`Отслеживание ${subscribed ? 'включено' : 'выключено'}`}
|
||||
disabled={anonymous || processing}
|
||||
icon={
|
||||
subscribed ? (
|
||||
<FiBell size='1.25rem' className='clr-text-primary' />
|
||||
) : (
|
||||
<FiBellOff size='1.25rem' className='clr-text-controls' />
|
||||
)
|
||||
}
|
||||
style={{ outlineColor: 'transparent' }}
|
||||
onClick={controller.toggleSubscribe}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Стать владельцем'
|
||||
icon={<LuCrown size='1.25rem' className={!claimable || anonymous ? '' : 'clr-text-success'} />}
|
||||
disabled={!claimable || anonymous || processing}
|
||||
onClick={controller.claim}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Удалить схему'
|
||||
disabled={!controller.isMutable}
|
||||
onClick={onDestroy}
|
||||
icon={<BiTrash size='1.25rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
|
||||
/>
|
||||
{!anonymous ? (
|
||||
<MiniButton
|
||||
title={`Отслеживание ${subscribed ? 'включено' : 'выключено'}`}
|
||||
disabled={processing}
|
||||
icon={
|
||||
subscribed ? (
|
||||
<FiBell size='1.25rem' className='icon-primary' />
|
||||
) : (
|
||||
<FiBellOff size='1.25rem' className='clr-text-controls' />
|
||||
)
|
||||
}
|
||||
onClick={controller.toggleSubscribe}
|
||||
/>
|
||||
) : null}
|
||||
{!anonymous && claimable ? (
|
||||
<MiniButton
|
||||
title='Стать владельцем'
|
||||
icon={<LuCrown size='1.25rem' className='icon-green' />}
|
||||
disabled={processing}
|
||||
onClick={controller.claim}
|
||||
/>
|
||||
) : null}
|
||||
{controller.isContentEditable ? (
|
||||
<MiniButton
|
||||
title='Удалить схему'
|
||||
disabled={!controller.isMutable}
|
||||
onClick={onDestroy}
|
||||
icon={<BiTrash size='1.25rem' className='icon-red' />}
|
||||
/>
|
||||
) : null}
|
||||
<HelpButton topic={HelpTopic.RSFORM} offset={4} />
|
||||
</Overlay>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { type RowSelectionState } from '@/components/DataTable';
|
||||
|
@ -92,17 +93,24 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
|
|||
|
||||
return (
|
||||
<div tabIndex={-1} className='outline-none' onKeyDown={handleTableKey}>
|
||||
<SelectedCounter
|
||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||
selectedCount={selected.length}
|
||||
position='top-[0.3rem] left-2'
|
||||
{controller.isContentEditable ? (
|
||||
<SelectedCounter
|
||||
totalCount={controller.schema?.stats?.count_all ?? 0}
|
||||
selectedCount={selected.length}
|
||||
position='top-[0.3rem] left-2'
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{controller.isContentEditable ? <RSListToolbar selectedCount={selected.length} /> : null}
|
||||
<div
|
||||
className={clsx('border-b', {
|
||||
'pt-[2.3rem]': controller.isContentEditable,
|
||||
'relative top-[-1px]': !controller.isContentEditable
|
||||
})}
|
||||
/>
|
||||
|
||||
<RSListToolbar selectedCount={selected.length} />
|
||||
|
||||
<div className='pt-[2.3rem] border-b' />
|
||||
|
||||
<RSTable
|
||||
enableSelection={controller.isContentEditable}
|
||||
items={controller.schema?.items}
|
||||
selected={rowSelection}
|
||||
setSelected={handleRowSelection}
|
||||
|
|
|
@ -47,17 +47,14 @@ function RSListToolbar({ selectedCount }: RSListToolbarProps) {
|
|||
<MiniButton
|
||||
title='Клонировать конституенту [Alt + V]'
|
||||
icon={
|
||||
<BiDuplicate
|
||||
size='1.25rem'
|
||||
className={controller.isMutable && selectedCount === 1 ? 'clr-text-success' : ''}
|
||||
/>
|
||||
<BiDuplicate size='1.25rem' className={controller.isMutable && selectedCount === 1 ? 'clr-text-green' : ''} />
|
||||
}
|
||||
disabled={!controller.isMutable || selectedCount !== 1}
|
||||
onClick={controller.cloneCst}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Добавить новую конституенту... [Alt + `]'
|
||||
icon={<BiPlusCircle size='1.25rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||
icon={<BiPlusCircle size='1.25rem' className={controller.isMutable ? 'clr-text-green' : ''} />}
|
||||
disabled={!controller.isMutable}
|
||||
onClick={() => controller.createCst(undefined, false)}
|
||||
/>
|
||||
|
@ -65,7 +62,7 @@ function RSListToolbar({ selectedCount }: RSListToolbarProps) {
|
|||
<MiniButton
|
||||
title='Добавить пустую конституенту'
|
||||
hideTitle={insertMenu.isOpen}
|
||||
icon={<BiDownArrowCircle size='1.25rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||
icon={<BiDownArrowCircle size='1.25rem' className={controller.isMutable ? 'clr-text-green' : ''} />}
|
||||
disabled={!controller.isMutable}
|
||||
onClick={insertMenu.toggle}
|
||||
/>
|
||||
|
@ -82,7 +79,7 @@ function RSListToolbar({ selectedCount }: RSListToolbarProps) {
|
|||
</div>
|
||||
<MiniButton
|
||||
title='Удалить выбранные [Delete]'
|
||||
icon={<BiTrash size='1.25rem' className={controller.isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}
|
||||
icon={<BiTrash size='1.25rem' className={controller.isMutable && !nothingSelected ? 'clr-text-red' : ''} />}
|
||||
disabled={!controller.isMutable || nothingSelected}
|
||||
onClick={controller.deleteCst}
|
||||
/>
|
||||
|
|
|
@ -14,6 +14,7 @@ import { labelCstTypification } from '@/utils/labels';
|
|||
|
||||
interface RSTableProps {
|
||||
items?: IConstituenta[];
|
||||
enableSelection: boolean;
|
||||
selected: RowSelectionState;
|
||||
setSelected: React.Dispatch<React.SetStateAction<RowSelectionState>>;
|
||||
|
||||
|
@ -28,7 +29,7 @@ const COLUMN_CONVENTION_HIDE_THRESHOLD = 1800;
|
|||
|
||||
const columnHelper = createColumnHelper<IConstituenta>();
|
||||
|
||||
function RSTable({ items, selected, setSelected, onEdit, onCreateNew }: RSTableProps) {
|
||||
function RSTable({ items, enableSelection, selected, setSelected, onEdit, onCreateNew }: RSTableProps) {
|
||||
const { colors, noNavigation } = useConceptTheme();
|
||||
const windowSize = useWindowSize();
|
||||
|
||||
|
@ -132,7 +133,7 @@ function RSTable({ items, selected, setSelected, onEdit, onCreateNew }: RSTableP
|
|||
enableHiding
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
enableRowSelection
|
||||
enableRowSelection={enableSelection}
|
||||
rowSelection={selected}
|
||||
onRowSelectionChange={setSelected}
|
||||
noDataComponent={
|
||||
|
|
|
@ -48,7 +48,7 @@ function GraphToolbar({
|
|||
title={!noText ? 'Скрыть текст' : 'Отобразить текст'}
|
||||
icon={
|
||||
!noText ? (
|
||||
<BiFontFamily size='1.25rem' className='clr-text-success' />
|
||||
<BiFontFamily size='1.25rem' className='clr-text-green' />
|
||||
) : (
|
||||
<BiFont size='1.25rem' className='clr-text-primary' />
|
||||
)
|
||||
|
@ -61,23 +61,26 @@ function GraphToolbar({
|
|||
onClick={onResetViewpoint}
|
||||
/>
|
||||
<MiniButton
|
||||
icon={<BiPlanet size='1.25rem' className={!is3D ? '' : orbit ? 'clr-text-success' : 'clr-text-primary'} />}
|
||||
icon={<BiPlanet size='1.25rem' className={orbit ? 'icon-green' : 'icon-primary'} />}
|
||||
title='Анимация вращения'
|
||||
disabled={!is3D}
|
||||
onClick={toggleOrbit}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Новая конституента'
|
||||
icon={<BiPlusCircle size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
|
||||
disabled={!isMutable}
|
||||
onClick={onCreate}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Удалить выбранные'
|
||||
icon={<BiTrash size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}
|
||||
disabled={!isMutable || nothingSelected}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
{isMutable ? (
|
||||
<MiniButton
|
||||
title='Новая конституента'
|
||||
icon={<BiPlusCircle size='1.25rem' className='icon-green' />}
|
||||
onClick={onCreate}
|
||||
/>
|
||||
) : null}
|
||||
{isMutable ? (
|
||||
<MiniButton
|
||||
title='Удалить выбранные'
|
||||
icon={<BiTrash size='1.25rem' className='icon-red' />}
|
||||
disabled={nothingSelected}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
) : null}
|
||||
<HelpButton topic={HelpTopic.GRAPH_TERM} className='max-w-[calc(100vw-4rem)]' offset={4} />
|
||||
</Overlay>
|
||||
);
|
||||
|
|
|
@ -12,15 +12,19 @@ import Loader from '@/components/ui/Loader';
|
|||
import TextURL from '@/components/ui/TextURL';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem';
|
||||
import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate';
|
||||
import DlgCreateCst from '@/dialogs/DlgCreateCst';
|
||||
import DlgCreateVersion from '@/dialogs/DlgCreateVersion';
|
||||
import DlgDeleteCst from '@/dialogs/DlgDeleteCst';
|
||||
import DlgEditVersions from '@/dialogs/DlgEditVersions';
|
||||
import DlgEditWordForms from '@/dialogs/DlgEditWordForms';
|
||||
import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
||||
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
||||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||
import { IVersionData } from '@/models/library';
|
||||
import { UserAccessMode } from '@/models/miscellaneous';
|
||||
import {
|
||||
CstType,
|
||||
|
@ -40,6 +44,9 @@ import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
|||
interface IRSEditContext {
|
||||
schema?: IRSForm;
|
||||
isMutable: boolean;
|
||||
isContentEditable: boolean;
|
||||
|
||||
viewVersion: (version?: number) => void;
|
||||
|
||||
moveUp: () => void;
|
||||
moveDown: () => void;
|
||||
|
@ -58,6 +65,9 @@ interface IRSEditContext {
|
|||
download: () => void;
|
||||
reindex: () => void;
|
||||
substitute: () => void;
|
||||
|
||||
createVersion: () => void;
|
||||
editVersions: () => void;
|
||||
}
|
||||
|
||||
const RSEditContext = createContext<IRSEditContext | null>(null);
|
||||
|
@ -89,6 +99,7 @@ export const RSEditState = ({
|
|||
onDeleteCst,
|
||||
children
|
||||
}: RSEditStateProps) => {
|
||||
const router = useConceptNavigation();
|
||||
const { user } = useAuth();
|
||||
const { mode, setMode } = useAccessMode();
|
||||
const model = useRSForm();
|
||||
|
@ -102,11 +113,15 @@ export const RSEditState = ({
|
|||
);
|
||||
}, [user?.is_staff, mode, model.isOwned, model.loading, model.processing]);
|
||||
|
||||
const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]);
|
||||
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [showClone, setShowClone] = useState(false);
|
||||
const [showDeleteCst, setShowDeleteCst] = useState(false);
|
||||
const [showEditTerm, setShowEditTerm] = useState(false);
|
||||
const [showSubstitute, setShowSubstitute] = useState(false);
|
||||
const [showCreateVersion, setShowCreateVersion] = useState(false);
|
||||
const [showEditVersions, setShowEditVersions] = useState(false);
|
||||
|
||||
const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
|
||||
const [showCreateCst, setShowCreateCst] = useState(false);
|
||||
|
@ -131,6 +146,17 @@ export const RSEditState = ({
|
|||
[model.schema, setMode, model.isOwned]
|
||||
);
|
||||
|
||||
const viewVersion = useCallback(
|
||||
(version?: number) => {
|
||||
if (version) {
|
||||
router.push(`/rsforms/${model.schemaID}?v=${version}`);
|
||||
} else {
|
||||
router.push(`/rsforms/${model.schemaID}`);
|
||||
}
|
||||
},
|
||||
[router, model]
|
||||
);
|
||||
|
||||
const handleCreateCst = useCallback(
|
||||
(data: ICstCreateData) => {
|
||||
if (!model.schema) {
|
||||
|
@ -196,6 +222,39 @@ export const RSEditState = ({
|
|||
[model, activeCst]
|
||||
);
|
||||
|
||||
const handleCreateVersion = useCallback(
|
||||
(data: IVersionData) => {
|
||||
if (!model.schema) {
|
||||
return;
|
||||
}
|
||||
model.versionCreate(data, newVersion => {
|
||||
toast.success('Версия создана');
|
||||
viewVersion(newVersion);
|
||||
});
|
||||
},
|
||||
[model, viewVersion]
|
||||
);
|
||||
|
||||
const handleDeleteVersion = useCallback(
|
||||
(versionID: number) => {
|
||||
if (!model.schema) {
|
||||
return;
|
||||
}
|
||||
model.versionDelete(versionID, () => toast.success('Версия удалена'));
|
||||
},
|
||||
[model]
|
||||
);
|
||||
|
||||
const handleUpdateVersion = useCallback(
|
||||
(versionID: number, data: IVersionData) => {
|
||||
if (!model.schema) {
|
||||
return;
|
||||
}
|
||||
model.versionUpdate(versionID, data, () => toast.success('Версия обновлена'));
|
||||
},
|
||||
[model]
|
||||
);
|
||||
|
||||
const moveUp = useCallback(() => {
|
||||
if (!model.schema?.items || selected.length === 0) {
|
||||
return;
|
||||
|
@ -348,7 +407,8 @@ export const RSEditState = ({
|
|||
}, [model]);
|
||||
|
||||
const share = useCallback(() => {
|
||||
const url = window.location.href + '&share';
|
||||
const currentRef = window.location.href;
|
||||
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => toast.success(`Ссылка скопирована: ${url}`))
|
||||
|
@ -368,6 +428,9 @@ export const RSEditState = ({
|
|||
value={{
|
||||
schema: model.schema,
|
||||
isMutable,
|
||||
isContentEditable,
|
||||
|
||||
viewVersion,
|
||||
|
||||
moveUp,
|
||||
moveDown,
|
||||
|
@ -385,7 +448,10 @@ export const RSEditState = ({
|
|||
share,
|
||||
toggleSubscribe,
|
||||
reindex,
|
||||
substitute
|
||||
substitute,
|
||||
|
||||
createVersion: () => setShowCreateVersion(true),
|
||||
editVersions: () => setShowEditVersions(true)
|
||||
}}
|
||||
>
|
||||
{model.schema ? (
|
||||
|
@ -436,6 +502,21 @@ export const RSEditState = ({
|
|||
onCreate={handleCreateCst}
|
||||
/>
|
||||
) : null}
|
||||
{showCreateVersion ? (
|
||||
<DlgCreateVersion
|
||||
versions={model.schema.versions}
|
||||
hideWindow={() => setShowCreateVersion(false)}
|
||||
onCreate={handleCreateVersion}
|
||||
/>
|
||||
) : null}
|
||||
{showEditVersions ? (
|
||||
<DlgEditVersions
|
||||
versions={model.schema.versions}
|
||||
hideWindow={() => setShowEditVersions(false)}
|
||||
onDelete={handleDeleteVersion}
|
||||
onUpdate={handleUpdateVersion}
|
||||
/>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { useConceptTheme } from '@/context/ThemeContext';
|
|||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { IConstituenta, IConstituentaMeta } from '@/models/rsform';
|
||||
import { prefixes, TIMEOUT_UI_REFRESH } from '@/utils/constants';
|
||||
import { labelVersion } from '@/utils/labels';
|
||||
|
||||
import EditorConstituenta from './EditorConstituenta';
|
||||
import EditorRSForm from './EditorRSForm';
|
||||
|
@ -33,6 +34,7 @@ function RSTabs() {
|
|||
const router = useConceptNavigation();
|
||||
const query = useQueryStrings();
|
||||
const activeTab = (Number(query.get('tab')) ?? RSTabID.CARD) as RSTabID;
|
||||
const version = Number(query.get('v')) ?? undefined;
|
||||
const cstQuery = query.get('active');
|
||||
|
||||
const { schema, loading } = useRSForm();
|
||||
|
@ -82,20 +84,21 @@ function RSTabs() {
|
|||
if (!schema) {
|
||||
return;
|
||||
}
|
||||
const versionStr = version ? `v=${version}&` : '';
|
||||
if (activeID) {
|
||||
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
|
||||
router.replace(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`);
|
||||
router.replace(`/rsforms/${schema.id}?${versionStr}tab=${tab}&active=${activeID}`);
|
||||
} else {
|
||||
router.push(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`);
|
||||
router.push(`/rsforms/${schema.id}?${versionStr}tab=${tab}&active=${activeID}`);
|
||||
}
|
||||
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
|
||||
activeID = schema.items[0].id;
|
||||
router.replace(`/rsforms/${schema.id}?tab=${tab}&active=${activeID}`);
|
||||
router.replace(`/rsforms/${schema.id}?${versionStr}tab=${tab}&active=${activeID}`);
|
||||
} else {
|
||||
router.push(`/rsforms/${schema.id}?tab=${tab}`);
|
||||
router.push(`/rsforms/${schema.id}?${versionStr}tab=${tab}`);
|
||||
}
|
||||
},
|
||||
[router, schema, activeTab]
|
||||
[router, schema, activeTab, version]
|
||||
);
|
||||
|
||||
function onSelectTab(index: number) {
|
||||
|
@ -172,10 +175,13 @@ function RSTabs() {
|
|||
<TabList className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2')}>
|
||||
<RSTabsMenu onDestroy={onDestroySchema} />
|
||||
|
||||
<TabLabel className='' label='Карточка' title={`Название схемы: ${schema.title ?? ''}`} />
|
||||
<TabLabel
|
||||
label='Карточка'
|
||||
titleHtml={`Название: <b>${schema.title ?? ''}</b><br />Версия: ${labelVersion(schema)}`}
|
||||
/>
|
||||
<TabLabel
|
||||
label='Содержание'
|
||||
title={`Конституент: ${schema.stats?.count_all ?? 0} | Ошибок: ${schema.stats?.count_errors ?? 0}`}
|
||||
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${schema.stats?.count_errors ?? 0}`}
|
||||
/>
|
||||
<TabLabel label='Редактор' />
|
||||
<TabLabel label='Граф термов' />
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
BiUpload
|
||||
} from 'react-icons/bi';
|
||||
import { FiEdit } from 'react-icons/fi';
|
||||
import { LuAlertCircle, LuAlertTriangle, LuCrown, LuGlasses, LuReplace } from 'react-icons/lu';
|
||||
import { LuAlertTriangle, LuArchive, LuCrown, LuGlasses, LuReplace } from 'react-icons/lu';
|
||||
import { VscLibrary } from 'react-icons/vsc';
|
||||
|
||||
import Button from '@/components/ui/Button';
|
||||
|
@ -103,10 +103,6 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
router.push('/login');
|
||||
}
|
||||
|
||||
function handleGotoCurrent() {
|
||||
router.push(`/rsforms/${model.schemaID}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex'>
|
||||
<div ref={schemaMenu.ref}>
|
||||
|
@ -122,56 +118,64 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
onClick={schemaMenu.toggle}
|
||||
/>
|
||||
<Dropdown isOpen={schemaMenu.isOpen}>
|
||||
<DropdownButton
|
||||
disabled={(!user || !model.isClaimable) && !model.isOwned}
|
||||
text={model.isOwned ? 'Вы — владелец' : 'Стать владельцем'}
|
||||
icon={<LuCrown size='1rem' className={model.isOwned ? 'clr-text-success' : ''} />}
|
||||
onClick={!model.isOwned && user && model.isClaimable ? handleClaimOwner : undefined}
|
||||
/>
|
||||
{user ? (
|
||||
<DropdownButton
|
||||
disabled={!model.isClaimable && !model.isOwned}
|
||||
text={model.isOwned ? 'Вы — владелец' : 'Стать владельцем'}
|
||||
icon={<LuCrown size='1rem' className={model.isOwned ? 'clr-text-green' : ''} />}
|
||||
onClick={!model.isOwned && model.isClaimable ? handleClaimOwner : undefined}
|
||||
/>
|
||||
) : null}
|
||||
<DropdownButton
|
||||
text='Поделиться'
|
||||
icon={<BiShareAlt size='1rem' className='clr-text-primary' />}
|
||||
icon={<BiShareAlt size='1rem' className='icon-primary' />}
|
||||
onClick={handleShare}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!user}
|
||||
text='Клонировать'
|
||||
icon={<BiDuplicate size='1rem' className='clr-text-primary' />}
|
||||
onClick={handleClone}
|
||||
/>
|
||||
{user ? (
|
||||
<DropdownButton
|
||||
disabled={model.isArchive}
|
||||
text='Клонировать'
|
||||
icon={<BiDuplicate size='1rem' className='icon-primary' />}
|
||||
onClick={handleClone}
|
||||
/>
|
||||
) : null}
|
||||
<DropdownButton
|
||||
text='Выгрузить в Экстеор'
|
||||
icon={<BiDownload size='1rem' className='clr-text-primary' />}
|
||||
icon={<BiDownload size='1rem' className='icon-primary' />}
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!controller.isMutable}
|
||||
text='Загрузить из Экстеора'
|
||||
icon={<BiUpload size='1rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
|
||||
onClick={handleUpload}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!controller.isMutable}
|
||||
text='Удалить схему'
|
||||
icon={<BiTrash size='1rem' className={controller.isMutable ? 'clr-text-warning' : ''} />}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
|
||||
<DropdownButton
|
||||
className='border-t-2'
|
||||
text='Создать новую схему'
|
||||
icon={<BiPlusCircle size='1rem' className='clr-text-url' />}
|
||||
onClick={handleCreateNew}
|
||||
/>
|
||||
{user ? (
|
||||
<DropdownButton
|
||||
disabled={!controller.isContentEditable}
|
||||
text='Загрузить из Экстеора'
|
||||
icon={<BiUpload size='1rem' className='icon-red' />}
|
||||
onClick={handleUpload}
|
||||
/>
|
||||
) : null}
|
||||
{controller.isMutable ? (
|
||||
<DropdownButton
|
||||
text='Удалить схему'
|
||||
icon={<BiTrash size='1rem' className='icon-red' />}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
) : null}
|
||||
{user ? (
|
||||
<DropdownButton
|
||||
className='border-t-2'
|
||||
text='Создать новую схему'
|
||||
icon={<BiPlusCircle size='1rem' className='icon-primary' />}
|
||||
onClick={handleCreateNew}
|
||||
/>
|
||||
) : null}
|
||||
<DropdownButton
|
||||
text='Библиотека'
|
||||
icon={<VscLibrary size='1rem' className='clr-text-url' />}
|
||||
icon={<VscLibrary size='1rem' className='icon-primary' />}
|
||||
onClick={() => router.push('/library')}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{!model.isArchive ? (
|
||||
{!model.isArchive && user ? (
|
||||
<div ref={editMenu.ref}>
|
||||
<Button
|
||||
dense
|
||||
|
@ -181,50 +185,45 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
title={'Редактирование'}
|
||||
hideTitle={editMenu.isOpen}
|
||||
className='h-full'
|
||||
icon={
|
||||
<FiEdit
|
||||
size='1.25rem'
|
||||
className={!user ? 'clr-text-controls' : controller.isMutable ? 'clr-text-success' : 'clr-text-warning'}
|
||||
/>
|
||||
}
|
||||
icon={<FiEdit size='1.25rem' className={controller.isContentEditable ? 'icon-green' : 'icon-red'} />}
|
||||
onClick={editMenu.toggle}
|
||||
/>
|
||||
<Dropdown isOpen={editMenu.isOpen}>
|
||||
<DropdownButton
|
||||
disabled={!controller.isMutable}
|
||||
disabled={!controller.isContentEditable}
|
||||
text='Сброс имён'
|
||||
title='Присвоить порядковые имена и обновить выражения'
|
||||
icon={<BiAnalyse size='1rem' className={controller.isMutable ? 'clr-text-primary' : ''} />}
|
||||
icon={<BiAnalyse size='1rem' className='icon-primary' />}
|
||||
onClick={handleReindex}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!controller.isMutable}
|
||||
disabled={!controller.isContentEditable}
|
||||
text='Банк выражений'
|
||||
title='Создать конституенту из шаблона'
|
||||
icon={<BiDiamond size='1rem' className={controller.isMutable ? 'clr-text-success' : ''} />}
|
||||
icon={<BiDiamond size='1rem' className='icon-green' />}
|
||||
onClick={handleTemplates}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!controller.isMutable}
|
||||
disabled={!controller.isContentEditable}
|
||||
text='Отождествление'
|
||||
title='Заменить вхождения одной конституенты на другую'
|
||||
icon={<LuReplace size='1rem' className={controller.isMutable ? 'clr-text-primary' : ''} />}
|
||||
icon={<LuReplace size='1rem' className='icon-primary' />}
|
||||
onClick={handleSubstituteCst}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
) : null}
|
||||
{model.isArchive ? (
|
||||
{model.isArchive && user ? (
|
||||
<Button
|
||||
dense
|
||||
noBorder
|
||||
noOutline
|
||||
tabIndex={-1}
|
||||
title={'Редактирование запрещено - Архив'}
|
||||
titleHtml='<b>Архив</b>: Редактирование запрещено<br />Перейти к актуальной версии'
|
||||
hideTitle={accessMenu.isOpen}
|
||||
className='h-full'
|
||||
icon={<LuAlertCircle size='1.25rem' className='clr-text-primary' />}
|
||||
onClick={handleGotoCurrent}
|
||||
icon={<LuArchive size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => controller.viewVersion(undefined)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
@ -240,11 +239,11 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
className='h-full pr-2'
|
||||
icon={
|
||||
mode === UserAccessMode.ADMIN ? (
|
||||
<BiMeteor size='1.25rem' className='clr-text-primary' />
|
||||
<BiMeteor size='1.25rem' className='icon-primary' />
|
||||
) : mode === UserAccessMode.OWNER ? (
|
||||
<LuCrown size='1.25rem' className='clr-text-primary' />
|
||||
<LuCrown size='1.25rem' className='icon-primary' />
|
||||
) : (
|
||||
<LuGlasses size='1.25rem' className='clr-text-primary' />
|
||||
<LuGlasses size='1.25rem' className='icon-primary' />
|
||||
)
|
||||
}
|
||||
onClick={accessMenu.toggle}
|
||||
|
@ -253,21 +252,21 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
<DropdownButton
|
||||
text={labelAccessMode(UserAccessMode.READER)}
|
||||
title={describeAccessMode(UserAccessMode.READER)}
|
||||
icon={<LuGlasses size='1rem' className='clr-text-primary' />}
|
||||
icon={<LuGlasses size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleChangeMode(UserAccessMode.READER)}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!model.isOwned}
|
||||
text={labelAccessMode(UserAccessMode.OWNER)}
|
||||
title={describeAccessMode(UserAccessMode.OWNER)}
|
||||
icon={<LuCrown size='1rem' className={model.isOwned ? 'clr-text-primary' : ''} />}
|
||||
icon={<LuCrown size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleChangeMode(UserAccessMode.OWNER)}
|
||||
/>
|
||||
<DropdownButton
|
||||
disabled={!user?.is_staff}
|
||||
text={labelAccessMode(UserAccessMode.ADMIN)}
|
||||
title={describeAccessMode(UserAccessMode.ADMIN)}
|
||||
icon={<BiMeteor size='1rem' className={user?.is_staff ? 'clr-text-primary' : ''} />}
|
||||
icon={<BiMeteor size='1rem' className='icon-primary' />}
|
||||
onClick={() => handleChangeMode(UserAccessMode.ADMIN)}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
@ -279,10 +278,10 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
|
|||
noBorder
|
||||
noOutline
|
||||
tabIndex={-1}
|
||||
title={'Анонимный режим. Чтобы использовать все функции войдите в Портал'}
|
||||
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
|
||||
hideTitle={accessMenu.isOpen}
|
||||
className='h-full pr-2'
|
||||
icon={<LuAlertTriangle size='1.25rem' className='clr-text-warning' />}
|
||||
icon={<LuAlertTriangle size='1.25rem' className='icon-red' />}
|
||||
onClick={handleLogin}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { classnames } from '@/utils/constants';
|
|||
|
||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
|
||||
return <div className='mt-6 text-sm select-text clr-text-warning'>Данный email не используется на Портале.</div>;
|
||||
return <div className='mt-6 text-sm select-text clr-text-red'>Данный email не используется на Портале.</div>;
|
||||
} else {
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { IUserUpdatePassword } from '@/models/library';
|
|||
|
||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
|
||||
return <div className='text-sm select-text clr-text-warning'>Неверно введен старый пароль</div>;
|
||||
return <div className='text-sm select-text clr-text-red'>Неверно введен старый пароль</div>;
|
||||
} else {
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ function ViewSubscriptions({ items }: ViewSubscriptionsProps) {
|
|||
size: 150,
|
||||
maxSize: 150,
|
||||
cell: props => (
|
||||
<div className='text-sm whitespace-nowrap'>{new Date(props.cell.getValue()).toLocaleString(intl.locale)}</div>
|
||||
<div className='text-sm whitespace-nowrap'>{new Date(props.getValue()).toLocaleString(intl.locale)}</div>
|
||||
),
|
||||
enableSorting: true
|
||||
})
|
||||
|
|
|
@ -161,17 +161,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
.clr-text-warning {
|
||||
.clr-text-red {
|
||||
color: var(--cl-red-fg-100);
|
||||
.dark & {
|
||||
color: var(--cd-red-fg-100);
|
||||
}
|
||||
}
|
||||
|
||||
.clr-text-success {
|
||||
.clr-text-green {
|
||||
color: var(--cl-green-fg-100);
|
||||
.dark & {
|
||||
color: var(--cd-green-fg-100);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-primary {
|
||||
:not([disabled]) > & {
|
||||
@apply clr-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-red {
|
||||
:not([disabled]) > & {
|
||||
@apply clr-text-red;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-green {
|
||||
:not([disabled]) > & {
|
||||
@apply clr-text-green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -226,8 +226,8 @@ export function getRSFormDetails(target: string, version: string, request: Front
|
|||
});
|
||||
} else {
|
||||
AxiosGet({
|
||||
title: `RSForm details for id=${target}`,
|
||||
endpoint: `/api/rsforms/${target}/versions/{version}`,
|
||||
title: `RSForm details for id=${target} version=${version}`,
|
||||
endpoint: `/api/rsforms/${target}/versions/${version}`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
@ -273,13 +273,22 @@ export function deleteUnsubscribe(target: string, request: FrontAction) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getTRSFile(target: string, request: FrontPull<Blob>) {
|
||||
AxiosGet({
|
||||
title: `RSForm TRS file for id=${target}`,
|
||||
endpoint: `/api/rsforms/${target}/export-trs`,
|
||||
request: request,
|
||||
options: { responseType: 'blob' }
|
||||
});
|
||||
export function getTRSFile(target: string, version: string, request: FrontPull<Blob>) {
|
||||
if (!version) {
|
||||
AxiosGet({
|
||||
title: `RSForm TRS file for id=${target}`,
|
||||
endpoint: `/api/rsforms/${target}/export-trs`,
|
||||
request: request,
|
||||
options: { responseType: 'blob' }
|
||||
});
|
||||
} else {
|
||||
AxiosGet({
|
||||
title: `RSForm TRS file for id=${target} version=${version}`,
|
||||
endpoint: `/api/versions/${version}/export-file`,
|
||||
request: request,
|
||||
options: { responseType: 'blob' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function postNewConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
||||
import { CstMatchMode, DependencyMode, HelpTopic, LibraryFilterStrategy, UserAccessMode } from '@/models/miscellaneous';
|
||||
import { CstClass, CstType, ExpressionStatus, IConstituenta } from '@/models/rsform';
|
||||
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import {
|
||||
IArgumentInfo,
|
||||
IRSErrorDescription,
|
||||
|
@ -62,6 +62,14 @@ export function labelConstituenta(cst: IConstituenta) {
|
|||
return `${cst.alias}: ${describeConstituenta(cst)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates label for {@link IVersionInfo} of {@link IRSForm}.
|
||||
*/
|
||||
export function labelVersion(schema?: IRSForm) {
|
||||
const version = schema?.versions.find(ver => ver.id === schema.version);
|
||||
return version ? version.version : 'актуальная';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves label for {@link TokenID}.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue
Block a user