F: Implement location editing + showSubfolders
This commit is contained in:
parent
118c5459f3
commit
37d022a030
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -200,6 +200,7 @@
|
|||
"Пакулина",
|
||||
"пересинтез",
|
||||
"Персиц",
|
||||
"подпапках",
|
||||
"Присакарь",
|
||||
"ПРОКСИМА",
|
||||
"Родоструктурная",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
''' REST API: Serializers. '''
|
||||
|
||||
from .basics import AccessPolicySerializer, LocationSerializer
|
||||
from .basics import AccessPolicySerializer, LocationSerializer, RenameLocationSerializer
|
||||
from .data_access import (
|
||||
LibraryItemBaseSerializer,
|
||||
LibraryItemCloneSerializer,
|
||||
|
|
|
@ -19,6 +19,24 @@ class LocationSerializer(serializers.Serializer):
|
|||
return attrs
|
||||
|
||||
|
||||
class RenameLocationSerializer(serializers.Serializer):
|
||||
''' Serializer: rename location. '''
|
||||
target = serializers.CharField(max_length=500)
|
||||
new_location = serializers.CharField(max_length=500)
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
if not validate_location(attrs['target']):
|
||||
raise serializers.ValidationError({
|
||||
'target': msg.invalidLocation()
|
||||
})
|
||||
if not validate_location(attrs['target']):
|
||||
raise serializers.ValidationError({
|
||||
'new_location': msg.invalidLocation()
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
class AccessPolicySerializer(serializers.Serializer):
|
||||
''' Serializer: Constituenta renaming. '''
|
||||
access_policy = serializers.CharField()
|
||||
|
|
|
@ -181,6 +181,42 @@ class TestLibraryViewset(EndpointTester):
|
|||
self.unowned.refresh_from_db()
|
||||
self.assertEqual(self.unowned.location, data['location'])
|
||||
|
||||
@decl_endpoint('/api/library/rename-location', method='patch')
|
||||
def test_rename_location(self):
|
||||
self.owned.location = '/U/temp'
|
||||
self.owned.save()
|
||||
self.unowned.location = '/U/temp'
|
||||
self.unowned.save()
|
||||
owned2 = LibraryItem.objects.create(
|
||||
title='Test3',
|
||||
alias='T3',
|
||||
owner=self.user,
|
||||
location='/U/temp/123'
|
||||
)
|
||||
|
||||
data = {
|
||||
'target': '/U/temp',
|
||||
'new_location': '/U/temp2'
|
||||
}
|
||||
|
||||
self.executeBadData(data={})
|
||||
self.executeBadData(data={'target:': '/U/temp'})
|
||||
self.executeBadData(data={'new_location:': '/U/temp'})
|
||||
self.executeBadData(data={'target:': 'invalid', 'new_location': '/U/temp'})
|
||||
self.executeBadData(data={'target:': '/U/temp', 'new_location': 'invalid'})
|
||||
self.executeOK(data=data)
|
||||
self.owned.refresh_from_db()
|
||||
self.unowned.refresh_from_db()
|
||||
owned2.refresh_from_db()
|
||||
self.assertEqual(self.owned.location, '/U/temp2')
|
||||
self.assertEqual(self.unowned.location, '/U/temp')
|
||||
self.assertEqual(owned2.location, '/U/temp2/123')
|
||||
|
||||
self.toggle_admin(True)
|
||||
self.executeOK(data=data)
|
||||
self.unowned.refresh_from_db()
|
||||
self.assertEqual(self.unowned.location, '/U/temp2')
|
||||
|
||||
@decl_endpoint('/api/library/{item}/set-editors', method='patch')
|
||||
def test_set_editors(self):
|
||||
time_update = self.owned.time_update
|
||||
|
|
|
@ -82,7 +82,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
access_level = permissions.ItemOwner
|
||||
elif self.action in [
|
||||
'create',
|
||||
'clone'
|
||||
'clone',
|
||||
'rename_location'
|
||||
]:
|
||||
access_level = permissions.GlobalUser
|
||||
else:
|
||||
|
@ -92,6 +93,42 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
def _get_item(self) -> m.LibraryItem:
|
||||
return cast(m.LibraryItem, self.get_object())
|
||||
|
||||
@extend_schema(
|
||||
summary='rename location',
|
||||
tags=['Library'],
|
||||
request=s.RenameLocationSerializer,
|
||||
responses={
|
||||
c.HTTP_200_OK: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=False, methods=['patch'], url_path='rename-location')
|
||||
def rename_location(self, request: Request) -> HttpResponse:
|
||||
''' Endpoint: Rename location. '''
|
||||
serializer = s.RenameLocationSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
target = serializer.validated_data['target']
|
||||
new_location = serializer.validated_data['new_location']
|
||||
if target == new_location:
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
if new_location.startswith(m.LocationHead.LIBRARY) and not self.request.user.is_staff:
|
||||
return Response(status=c.HTTP_403_FORBIDDEN)
|
||||
|
||||
with transaction.atomic():
|
||||
changed: list[m.LibraryItem] = []
|
||||
items = m.LibraryItem.objects \
|
||||
.filter(Q(location=target) | Q(location__startswith=f'{target}/')) \
|
||||
.only('location', 'owner_id')
|
||||
for item in items:
|
||||
if item.owner_id == self.request.user.pk or self.request.user.is_staff:
|
||||
item.location = item.location.replace(target, new_location)
|
||||
changed.append(item)
|
||||
if changed:
|
||||
m.LibraryItem.objects.bulk_update(changed, ['location'])
|
||||
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
summary='clone item including contents',
|
||||
tags=['Library'],
|
||||
|
|
|
@ -39,7 +39,7 @@ def can_edit_item(user, obj: Any) -> bool:
|
|||
return True
|
||||
|
||||
item = _extract_item(obj)
|
||||
if item.owner == user:
|
||||
if item.owner_id == user.pk:
|
||||
return True
|
||||
|
||||
if Editor.objects.filter(
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
ILibraryCreateData,
|
||||
ILibraryItem,
|
||||
ILibraryUpdateData,
|
||||
IRenameLocationData,
|
||||
ITargetAccessPolicy,
|
||||
ITargetLocation,
|
||||
IVersionData
|
||||
|
@ -94,6 +95,13 @@ export function patchSetLocation(target: string, request: FrontPush<ITargetLocat
|
|||
});
|
||||
}
|
||||
|
||||
export function patchRenameLocation(request: FrontPush<IRenameLocationData>) {
|
||||
AxiosPatch({
|
||||
endpoint: `/api/library/rename-location`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function patchSetEditors(target: string, request: FrontPush<ITargetUsers>) {
|
||||
AxiosPatch({
|
||||
endpoint: `/api/library/${target}/set-editors`,
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
IconStatusIncalculable,
|
||||
IconStatusOK,
|
||||
IconStatusUnknown,
|
||||
IconSubfolders,
|
||||
IconTemplates,
|
||||
IconTerm,
|
||||
IconText,
|
||||
|
@ -62,6 +63,14 @@ export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconPr
|
|||
}
|
||||
}
|
||||
|
||||
export function SubfoldersIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||
if (value) {
|
||||
return <IconSubfolders size={size} className={className ?? 'clr-text-green'} />;
|
||||
} else {
|
||||
return <IconSubfolders size={size} className={className ?? 'clr-text-controls'} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function LocationIcon({ value, size = '1.25rem', className }: DomIconProps<string>) {
|
||||
switch (value.substring(0, 2) as LocationHead) {
|
||||
case LocationHead.COMMON:
|
||||
|
|
|
@ -34,6 +34,8 @@ export { LuMoon as IconDarkTheme } from 'react-icons/lu';
|
|||
export { LuSun as IconLightTheme } from 'react-icons/lu';
|
||||
export { LuFolderTree as IconFolderTree } from 'react-icons/lu';
|
||||
export { LuFolder as IconFolder } from 'react-icons/lu';
|
||||
export { LuFolders as IconSubfolders } from 'react-icons/lu';
|
||||
export { LuFolderEdit as IconFolderEdit } from 'react-icons/lu';
|
||||
export { LuFolderOpen as IconFolderOpened } from 'react-icons/lu';
|
||||
export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
|
||||
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
|
||||
|
|
|
@ -8,13 +8,14 @@ import {
|
|||
getAdminLibrary,
|
||||
getLibrary,
|
||||
getTemplates,
|
||||
patchRenameLocation,
|
||||
postCloneLibraryItem,
|
||||
postCreateLibraryItem
|
||||
} from '@/backend/library';
|
||||
import { getRSFormDetails, postRSFormFromFile } from '@/backend/rsforms';
|
||||
import { ErrorData } from '@/components/info/InfoError';
|
||||
import { FolderTree } from '@/models/FolderTree';
|
||||
import { ILibraryItem, LibraryItemID, LocationHead } from '@/models/library';
|
||||
import { ILibraryItem, IRenameLocationData, LibraryItemID, LocationHead } from '@/models/library';
|
||||
import { ILibraryCreateData } from '@/models/library';
|
||||
import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
|
||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||
|
@ -45,6 +46,7 @@ interface ILibraryContext {
|
|||
createItem: (data: ILibraryCreateData, callback?: DataCallback<ILibraryItem>) => void;
|
||||
cloneItem: (target: LibraryItemID, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => void;
|
||||
destroyItem: (target: LibraryItemID, callback?: () => void) => void;
|
||||
renameLocation: (data: IRenameLocationData, callback?: () => void) => void;
|
||||
|
||||
localUpdateItem: (data: ILibraryItem) => void;
|
||||
localUpdateTimestamp: (target: LibraryItemID) => void;
|
||||
|
@ -92,8 +94,14 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
|||
result = result.filter(item => item.location.startsWith(filter.head!));
|
||||
}
|
||||
if (filter.folderMode && filter.location) {
|
||||
if (filter.subfolders) {
|
||||
result = result.filter(
|
||||
item => item.location == filter.location || item.location.startsWith(filter.location! + '/')
|
||||
);
|
||||
} else {
|
||||
result = result.filter(item => item.location == filter.location);
|
||||
}
|
||||
}
|
||||
if (filter.type) {
|
||||
result = result.filter(item => item.item_type === filter.type);
|
||||
}
|
||||
|
@ -270,6 +278,23 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
|||
[reloadItems, user]
|
||||
);
|
||||
|
||||
const renameLocation = useCallback(
|
||||
(data: IRenameLocationData, callback?: () => void) => {
|
||||
setProcessingError(undefined);
|
||||
patchRenameLocation({
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setProcessingError,
|
||||
onSuccess: () =>
|
||||
reloadItems(() => {
|
||||
if (callback) callback();
|
||||
})
|
||||
});
|
||||
},
|
||||
[reloadItems, user]
|
||||
);
|
||||
|
||||
return (
|
||||
<LibraryContext.Provider
|
||||
value={{
|
||||
|
@ -290,6 +315,8 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
|||
createItem,
|
||||
cloneItem,
|
||||
destroyItem,
|
||||
renameLocation,
|
||||
|
||||
retrieveTemplate,
|
||||
localUpdateItem,
|
||||
localUpdateTimestamp
|
||||
|
|
|
@ -45,7 +45,7 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL
|
|||
onSubmit={() => onChangeLocation(location)}
|
||||
className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3')}
|
||||
>
|
||||
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
||||
<div className='flex flex-col gap-2 min-w-[7rem] h-min'>
|
||||
<Label className='select-none' text='Корень' />
|
||||
<SelectLocationHead
|
||||
value={head} // prettier: split-lines
|
||||
|
|
|
@ -132,6 +132,14 @@ export interface ITargetLocation {
|
|||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents update data for renaming Location.
|
||||
*/
|
||||
export interface IRenameLocationData {
|
||||
target: string;
|
||||
new_location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data, used for creating {@link IRSForm}.
|
||||
*/
|
||||
|
|
|
@ -180,6 +180,7 @@ export interface ILibraryFilter {
|
|||
query?: string;
|
||||
|
||||
folderMode?: boolean;
|
||||
subfolders?: boolean;
|
||||
path?: string;
|
||||
head?: LocationHead;
|
||||
location?: string;
|
||||
|
|
|
@ -2,14 +2,17 @@
|
|||
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import DataLoader from '@/components/wrap/DataLoader';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { ILibraryItem, LocationHead } from '@/models/library';
|
||||
import { ILibraryItem, IRenameLocationData, LocationHead } from '@/models/library';
|
||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||
import { storage } from '@/utils/constants';
|
||||
import { information } from '@/utils/labels';
|
||||
import { toggleTristateFlag } from '@/utils/utils';
|
||||
|
||||
import TableLibraryItems from './TableLibraryItems';
|
||||
|
@ -26,14 +29,12 @@ function LibraryPage() {
|
|||
|
||||
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
|
||||
const [folderMode, setFolderMode] = useLocalStorage<boolean>(storage.librarySearchFolderMode, true);
|
||||
const [subfolders, setSubfolders] = useLocalStorage<boolean>(storage.librarySearchSubfolders, false);
|
||||
const [location, setLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
|
||||
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
|
||||
const [isSubscribed, setIsSubscribed] = useLocalStorage<boolean | undefined>(
|
||||
storage.librarySearchSubscribed,
|
||||
undefined
|
||||
);
|
||||
const [isOwned, setIsOwned] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
|
||||
const [isEditor, setIsEditor] = useLocalStorage<boolean | undefined>(storage.librarySearchEditor, undefined);
|
||||
const [showRenameLocation, setShowRenameLocation] = useState(false);
|
||||
|
||||
const filter: ILibraryFilter = useMemo(
|
||||
() => ({
|
||||
|
@ -42,12 +43,12 @@ function LibraryPage() {
|
|||
query: query,
|
||||
isEditor: user ? isEditor : undefined,
|
||||
isOwned: user ? isOwned : undefined,
|
||||
isSubscribed: user ? isSubscribed : undefined,
|
||||
isVisible: user ? isVisible : true,
|
||||
folderMode: folderMode,
|
||||
subfolders: subfolders,
|
||||
location: location
|
||||
}),
|
||||
[head, path, query, isEditor, isOwned, isSubscribed, isVisible, user, folderMode, location]
|
||||
[head, path, query, isEditor, isOwned, isVisible, user, folderMode, location, subfolders]
|
||||
);
|
||||
|
||||
const hasCustomFilter = useMemo(
|
||||
|
@ -70,17 +71,35 @@ function LibraryPage() {
|
|||
const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]);
|
||||
const toggleEditor = useCallback(() => setIsEditor(prev => toggleTristateFlag(prev)), [setIsEditor]);
|
||||
const toggleFolderMode = useCallback(() => setFolderMode(prev => !prev), [setFolderMode]);
|
||||
const toggleSubfolders = useCallback(() => setSubfolders(prev => !prev), [setSubfolders]);
|
||||
|
||||
const resetFilter = useCallback(() => {
|
||||
setQuery('');
|
||||
setPath('');
|
||||
setHead(undefined);
|
||||
setIsVisible(true);
|
||||
setIsSubscribed(undefined);
|
||||
setIsOwned(undefined);
|
||||
setIsEditor(undefined);
|
||||
setLocation('');
|
||||
}, [setHead, setIsVisible, setIsSubscribed, setIsOwned, setIsEditor, setLocation]);
|
||||
}, [setHead, setIsVisible, setIsOwned, setIsEditor, setLocation]);
|
||||
|
||||
const promptRenameLocation = useCallback(() => {
|
||||
setShowRenameLocation(true);
|
||||
}, []);
|
||||
|
||||
const handleRenameLocation = useCallback(
|
||||
(newLocation: string) => {
|
||||
const data: IRenameLocationData = {
|
||||
target: location,
|
||||
new_location: newLocation
|
||||
};
|
||||
library.renameLocation(data, () => {
|
||||
setLocation(newLocation);
|
||||
toast.success(information.locationRenamed);
|
||||
});
|
||||
},
|
||||
[location, library]
|
||||
);
|
||||
|
||||
const viewLibrary = useMemo(
|
||||
() => (
|
||||
|
@ -99,11 +118,14 @@ function LibraryPage() {
|
|||
<ViewSideLocation
|
||||
active={location}
|
||||
setActive={setLocation}
|
||||
subfolders={subfolders}
|
||||
folderTree={library.folders}
|
||||
toggleFolderMode={toggleFolderMode}
|
||||
toggleSubfolders={toggleSubfolders}
|
||||
onRenameLocation={promptRenameLocation}
|
||||
/>
|
||||
),
|
||||
[location, library.folders, setLocation, toggleFolderMode]
|
||||
[location, library.folders, setLocation, toggleFolderMode, subfolders]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -113,6 +135,13 @@ function LibraryPage() {
|
|||
error={library.loadingError}
|
||||
hasNoData={library.items.length === 0}
|
||||
>
|
||||
{showRenameLocation ? (
|
||||
<DlgChangeLocation
|
||||
initial={location}
|
||||
onChangeLocation={handleRenameLocation}
|
||||
hideWindow={() => setShowRenameLocation(false)}
|
||||
/>
|
||||
) : null}
|
||||
<ToolbarSearch
|
||||
total={library.items.length ?? 0}
|
||||
filtered={items.length}
|
||||
|
|
|
@ -103,7 +103,9 @@ function ToolbarSearch({
|
|||
'clr-input'
|
||||
)}
|
||||
>
|
||||
<div className={clsx('px-3 pt-1 self-center', 'min-w-[5.5rem]', 'select-none', 'whitespace-nowrap')}>
|
||||
<div
|
||||
className={clsx('px-3 pt-1 self-center', 'min-w-[6rem] sm:min-w-[7rem]', 'select-none', 'whitespace-nowrap')}
|
||||
>
|
||||
{filtered} из {total}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,11 +3,14 @@ import { motion } from 'framer-motion';
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { IconFolderTree } from '@/components/Icons';
|
||||
import { SubfoldersIcon } from '@/components/DomainIcons';
|
||||
import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import { CProps } from '@/components/props';
|
||||
import SelectLocation from '@/components/select/SelectLocation';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
import { FolderNode, FolderTree } from '@/models/FolderTree';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
@ -17,12 +20,25 @@ import { information } from '@/utils/labels';
|
|||
|
||||
interface ViewSideLocationProps {
|
||||
folderTree: FolderTree;
|
||||
subfolders: boolean;
|
||||
active: string;
|
||||
setActive: React.Dispatch<React.SetStateAction<string>>;
|
||||
toggleFolderMode: () => void;
|
||||
toggleSubfolders: () => void;
|
||||
onRenameLocation: () => void;
|
||||
}
|
||||
|
||||
function ViewSideLocation({ folderTree, active, setActive: setActive, toggleFolderMode }: ViewSideLocationProps) {
|
||||
function ViewSideLocation({
|
||||
folderTree,
|
||||
active,
|
||||
subfolders,
|
||||
setActive: setActive,
|
||||
toggleFolderMode,
|
||||
toggleSubfolders,
|
||||
onRenameLocation
|
||||
}: ViewSideLocationProps) {
|
||||
const { user } = useAuth();
|
||||
const { items } = useLibrary();
|
||||
const windowSize = useWindowSize();
|
||||
const handleClickFolder = useCallback(
|
||||
(event: CProps.EventMouse, target: FolderNode) => {
|
||||
|
@ -40,6 +56,18 @@ function ViewSideLocation({ folderTree, active, setActive: setActive, toggleFold
|
|||
[setActive]
|
||||
);
|
||||
|
||||
const canRename = useMemo(() => {
|
||||
if (active.length <= 3 || !user) {
|
||||
return false;
|
||||
}
|
||||
if (user.is_staff) {
|
||||
return true;
|
||||
}
|
||||
const owned = items.filter(item => item.owner == user.id);
|
||||
const located = owned.filter(item => item.location == active || item.location.startsWith(`${active}/`));
|
||||
return located.length !== 0;
|
||||
}, [active, user, items]);
|
||||
|
||||
const animations = useMemo(() => animateSideMinWidth(windowSize.isSmall ? '10rem' : '15rem'), [windowSize]);
|
||||
|
||||
return (
|
||||
|
@ -56,12 +84,21 @@ function ViewSideLocation({ folderTree, active, setActive: setActive, toggleFold
|
|||
offset={5}
|
||||
place='right-start'
|
||||
/>
|
||||
<div className='cc-icons'>
|
||||
<MiniButton
|
||||
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
|
||||
titleHtml='<b>Редактирование пути</b><br/>Перемещаются только Ваши схемы<br/>в указанной папке (и подпапках)'
|
||||
onClick={onRenameLocation}
|
||||
disabled={!canRename}
|
||||
/>
|
||||
<MiniButton title='Вложенные папки' icon={<SubfoldersIcon value={subfolders} />} onClick={toggleSubfolders} />
|
||||
<MiniButton
|
||||
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
||||
title='Переключение в режим Поиск'
|
||||
onClick={toggleFolderMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SelectLocation
|
||||
value={active}
|
||||
folderTree={folderTree}
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
IconFilterReset,
|
||||
IconFolder,
|
||||
IconFolderClosed,
|
||||
IconFolderEdit,
|
||||
IconFolderEmpty,
|
||||
IconFolderOpened,
|
||||
IconFolderTree,
|
||||
|
@ -10,9 +11,12 @@ import {
|
|||
IconSearch,
|
||||
IconShow,
|
||||
IconSortAsc,
|
||||
IconSortDesc
|
||||
IconSortDesc,
|
||||
IconSubfolders
|
||||
} from '@/components/Icons';
|
||||
import LinkTopic from '@/components/ui/LinkTopic';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
function HelpLibrary() {
|
||||
const { colors } = useConceptOptions();
|
||||
|
@ -20,8 +24,10 @@ function HelpLibrary() {
|
|||
<div>
|
||||
<h1>Библиотека схем</h1>
|
||||
<p>
|
||||
В библиотеке собраны <IconRSForm size='1rem' className='inline-icon' /> системы определений (КС) <br />и
|
||||
<IconOSS size='1rem' className='inline-icon' /> операционные схемы синтеза (ОСС).
|
||||
В библиотеке собраны <IconRSForm size='1rem' className='inline-icon' />{' '}
|
||||
<LinkTopic text='концептуальные схемы' topic={HelpTopic.CC_SYSTEM} /> (КС) <br />и
|
||||
<IconOSS size='1rem' className='inline-icon' />{' '}
|
||||
<LinkTopic text='операционные схемы синтеза' topic={HelpTopic.CC_OSS} /> (ОСС).
|
||||
</p>
|
||||
|
||||
<li>
|
||||
|
@ -51,20 +57,26 @@ function HelpLibrary() {
|
|||
</li>
|
||||
|
||||
<h2>Режим: Проводник</h2>
|
||||
<li>клик по папке отображает справа файлы в ней</li>
|
||||
<li>
|
||||
<IconFolderEdit size='1rem' className='inline-icon' /> переименовать выбранную
|
||||
</li>
|
||||
<li>
|
||||
<IconSubfolders size='1rem' className='inline-icon icon-green' /> схемы во вложенных папках
|
||||
</li>
|
||||
<li>клик по папке отображает справа схемы в ней</li>
|
||||
<li>Ctrl + клик по папке копирует путь в буфер обмена</li>
|
||||
<li>клик по иконке сворачивает/разворачивает вложенные</li>
|
||||
<li>
|
||||
<IconFolderEmpty size='1rem' className='inline-icon clr-text-default' /> папка без файлов
|
||||
<IconFolderEmpty size='1rem' className='inline-icon clr-text-default' /> папка без схем
|
||||
</li>
|
||||
<li>
|
||||
<IconFolderEmpty size='1rem' className='inline-icon' /> папка с вложенными без файлов
|
||||
<IconFolderEmpty size='1rem' className='inline-icon' /> папка с вложенными без схем
|
||||
</li>
|
||||
<li>
|
||||
<IconFolder size='1rem' className='inline-icon' /> папка без вложенных
|
||||
</li>
|
||||
<li>
|
||||
<IconFolderClosed size='1rem' className='inline-icon' /> папка с вложенными и файлами
|
||||
<IconFolderClosed size='1rem' className='inline-icon' /> папка с вложенными и схемами
|
||||
</li>
|
||||
<li>
|
||||
<IconFolderOpened size='1rem' className='inline-icon icon-green' /> развернутая папка
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
--toastify-color-dark: var(--cd-bg-60);
|
||||
}
|
||||
|
||||
.cm-tooltip {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
resize: vertical;
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import { NodeType, Tree, TreeCursor } from '@lezer/common';
|
||||
import { EditorState, ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
||||
import { EditorState, ReactCodeMirrorRef, SelectionRange, TooltipView } from '@uiw/react-codemirror';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { ReferenceTokens } from '@/components/RefsInput/parse';
|
||||
|
@ -165,10 +165,9 @@ export function findReferenceAt(pos: number, state: EditorState) {
|
|||
/**
|
||||
* Create DOM tooltip for {@link Constituenta}.
|
||||
*/
|
||||
export function domTooltipConstituenta(cst?: IConstituenta, canClick?: boolean) {
|
||||
export function domTooltipConstituenta(cst?: IConstituenta, canClick?: boolean): TooltipView {
|
||||
const dom = document.createElement('div');
|
||||
dom.className = clsx(
|
||||
'z-topmost',
|
||||
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
||||
'dense',
|
||||
'p-2',
|
||||
|
@ -183,6 +182,8 @@ export function domTooltipConstituenta(cst?: IConstituenta, canClick?: boolean)
|
|||
dom.appendChild(text);
|
||||
} else {
|
||||
const alias = document.createElement('p');
|
||||
alias.className = 'font-math';
|
||||
alias.style.overflowWrap = 'anywhere';
|
||||
alias.innerHTML = `<b>${cst.alias}:</b> ${labelCstTypification(cst)}`;
|
||||
dom.appendChild(alias);
|
||||
|
||||
|
@ -244,10 +245,9 @@ export function domTooltipEntityReference(
|
|||
cst: IConstituenta | undefined,
|
||||
colors: IColorTheme,
|
||||
canClick?: boolean
|
||||
) {
|
||||
): TooltipView {
|
||||
const dom = document.createElement('div');
|
||||
dom.className = clsx(
|
||||
'z-topmost',
|
||||
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
||||
'dense',
|
||||
'p-2 flex flex-col',
|
||||
|
@ -303,10 +303,9 @@ export function domTooltipSyntacticReference(
|
|||
ref: ISyntacticReference,
|
||||
masterRef: string | undefined,
|
||||
canClick?: boolean
|
||||
) {
|
||||
): TooltipView {
|
||||
const dom = document.createElement('div');
|
||||
dom.className = clsx(
|
||||
'z-topmost',
|
||||
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
||||
'dense',
|
||||
'p-2 flex flex-col',
|
||||
|
|
|
@ -108,10 +108,10 @@ export const storage = {
|
|||
|
||||
librarySearchHead: 'library.search.head',
|
||||
librarySearchFolderMode: 'library.search.folder_mode',
|
||||
librarySearchSubfolders: 'library.search.subfolders',
|
||||
librarySearchLocation: 'library.search.location',
|
||||
librarySearchVisible: 'library.search.visible',
|
||||
librarySearchOwned: 'library.search.owned',
|
||||
librarySearchSubscribed: 'library.search.subscribed',
|
||||
librarySearchEditor: 'library.search.editor',
|
||||
libraryPagination: 'library.pagination',
|
||||
|
||||
|
|
|
@ -931,6 +931,7 @@ export const information = {
|
|||
moveComplete: 'Перемещение завершено',
|
||||
linkReady: 'Ссылка скопирована',
|
||||
versionRestored: 'Загрузка версии завершена',
|
||||
locationRenamed: 'Ваши схемы перемещены',
|
||||
cloneComplete: (alias: string) => `Копия создана: ${alias}`,
|
||||
|
||||
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
||||
|
|
Loading…
Reference in New Issue
Block a user