Compare commits
No commits in common. "c1f8553ff4a04722090cefefc16075ca1656b374" and "bcc88492cc161a569d85608cde6c0919c2923d26" have entirely different histories.
c1f8553ff4
...
bcc88492cc
|
@ -31,10 +31,7 @@ export { RiMenuFoldFill as IconMenuFold } from 'react-icons/ri';
|
||||||
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
|
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
|
||||||
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
|
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
|
||||||
export { LuSun as IconLightTheme } from 'react-icons/lu';
|
export { LuSun as IconLightTheme } from 'react-icons/lu';
|
||||||
export { LuFolderTree as IconFolderTree } from 'react-icons/lu';
|
export { FaRegFolder as IconFolder } from 'react-icons/fa';
|
||||||
export { FaRegFolder as IconFolder } from 'react-icons/fa6';
|
|
||||||
export { FaRegFolderOpen as IconFolderOpened } from 'react-icons/fa6';
|
|
||||||
export { FaRegFolderClosed as IconFolderClosed } from 'react-icons/fa6';
|
|
||||||
export { LuLightbulb as IconHelp } from 'react-icons/lu';
|
export { LuLightbulb as IconHelp } from 'react-icons/lu';
|
||||||
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
|
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
|
||||||
export { RiPushpinFill as IconPin } from 'react-icons/ri';
|
export { RiPushpinFill as IconPin } from 'react-icons/ri';
|
||||||
|
|
|
@ -82,7 +82,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
caret: colors.fgDefault
|
caret: colors.fgDefault
|
||||||
},
|
},
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: tags.name, color: colors.fgPurple, cursor: schema ? 'default' : cursor }, // GlobalID
|
{ tag: tags.name, color: colors.fgPurple, cursor: schema ? 'default' : 'text' }, // GlobalID
|
||||||
{ tag: tags.variableName, color: colors.fgGreen }, // LocalID
|
{ tag: tags.variableName, color: colors.fgGreen }, // LocalID
|
||||||
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical
|
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical
|
||||||
{ tag: tags.keyword, color: colors.fgBlue }, // keywords
|
{ tag: tags.keyword, color: colors.fgBlue }, // keywords
|
||||||
|
@ -92,7 +92,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets)
|
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets)
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
[disabled, colors, darkMode, schema, cursor]
|
[disabled, colors, darkMode, schema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const editorExtensions = useMemo(
|
const editorExtensions = useMemo(
|
||||||
|
@ -101,7 +101,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
RSLanguage,
|
RSLanguage,
|
||||||
ccBracketMatching(darkMode),
|
ccBracketMatching(darkMode),
|
||||||
...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]),
|
...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]),
|
||||||
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema, onOpenEdit !== undefined)])
|
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema)])
|
||||||
],
|
],
|
||||||
[darkMode, schema, noTooltip, onOpenEdit]
|
[darkMode, schema, noTooltip, onOpenEdit]
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { EditorView } from '@uiw/react-codemirror';
|
||||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||||
import { findAliasAt } from '@/utils/codemirror';
|
import { findAliasAt } from '@/utils/codemirror';
|
||||||
|
|
||||||
const navigationProducer = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void) => {
|
const globalsNavigation = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void) => {
|
||||||
return EditorView.domEventHandlers({
|
return EditorView.domEventHandlers({
|
||||||
click: (event: MouseEvent, view: EditorView) => {
|
click: (event: MouseEvent, view: EditorView) => {
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
|
@ -34,5 +34,5 @@ const navigationProducer = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID)
|
||||||
};
|
};
|
||||||
|
|
||||||
export function rsNavigation(schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void): Extension {
|
export function rsNavigation(schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void): Extension {
|
||||||
return [navigationProducer(schema, onOpenEdit)];
|
return [globalsNavigation(schema, onOpenEdit)];
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { IRSForm } from '@/models/rsform';
|
||||||
import { findAliasAt } from '@/utils/codemirror';
|
import { findAliasAt } from '@/utils/codemirror';
|
||||||
import { domTooltipConstituenta } from '@/utils/codemirror';
|
import { domTooltipConstituenta } from '@/utils/codemirror';
|
||||||
|
|
||||||
const tooltipProducer = (schema: IRSForm, canClick?: boolean) => {
|
const globalsHoverTooltip = (schema: IRSForm) => {
|
||||||
return hoverTooltip((view, pos) => {
|
return hoverTooltip((view, pos) => {
|
||||||
const { alias, start, end } = findAliasAt(pos, view.state);
|
const { alias, start, end } = findAliasAt(pos, view.state);
|
||||||
if (!alias) {
|
if (!alias) {
|
||||||
|
@ -16,11 +16,11 @@ const tooltipProducer = (schema: IRSForm, canClick?: boolean) => {
|
||||||
pos: start,
|
pos: start,
|
||||||
end: end,
|
end: end,
|
||||||
above: false,
|
above: false,
|
||||||
create: () => domTooltipConstituenta(cst, canClick)
|
create: () => domTooltipConstituenta(cst)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function rsHoverTooltip(schema: IRSForm, canClick?: boolean): Extension {
|
export function rsHoverTooltip(schema: IRSForm): Extension {
|
||||||
return [tooltipProducer(schema, canClick)];
|
return [globalsHoverTooltip(schema)];
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,10 @@ import Label from '@/components/ui/Label';
|
||||||
import { useConceptOptions } from '@/context/OptionsContext';
|
import { useConceptOptions } from '@/context/OptionsContext';
|
||||||
import DlgEditReference from '@/dialogs/DlgEditReference';
|
import DlgEditReference from '@/dialogs/DlgEditReference';
|
||||||
import { ReferenceType } from '@/models/language';
|
import { ReferenceType } from '@/models/language';
|
||||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
import { IRSForm } from '@/models/rsform';
|
||||||
import { CodeMirrorWrapper } from '@/utils/codemirror';
|
import { CodeMirrorWrapper } from '@/utils/codemirror';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { refsNavigation } from './clickNavigation';
|
|
||||||
import { NaturalLanguage, ReferenceTokens } from './parse';
|
import { NaturalLanguage, ReferenceTokens } from './parse';
|
||||||
import { RefEntity } from './parse/parser.terms';
|
import { RefEntity } from './parse/parser.terms';
|
||||||
import { refsHoverTooltip } from './tooltip';
|
import { refsHoverTooltip } from './tooltip';
|
||||||
|
@ -66,7 +65,6 @@ interface RefsInputInputProps
|
||||||
label?: string;
|
label?: string;
|
||||||
onChange?: (newValue: string) => void;
|
onChange?: (newValue: string) => void;
|
||||||
schema?: IRSForm;
|
schema?: IRSForm;
|
||||||
onOpenEdit?: (cstID: ConstituentaID) => void;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
|
@ -75,23 +73,7 @@ interface RefsInputInputProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
||||||
(
|
({ id, label, disabled, schema, initialValue, value, resolved, onFocus, onBlur, onChange, ...restProps }, ref) => {
|
||||||
{
|
|
||||||
id, // prettier: split-lines
|
|
||||||
label,
|
|
||||||
disabled,
|
|
||||||
schema,
|
|
||||||
onOpenEdit,
|
|
||||||
initialValue,
|
|
||||||
value,
|
|
||||||
resolved,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
onChange,
|
|
||||||
...restProps
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const { darkMode, colors } = useConceptOptions();
|
const { darkMode, colors } = useConceptOptions();
|
||||||
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
@ -132,10 +114,9 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.contentAttributes.of({ spellcheck: 'true' }),
|
EditorView.contentAttributes.of({ spellcheck: 'true' }),
|
||||||
NaturalLanguage,
|
NaturalLanguage,
|
||||||
...(!schema || !onOpenEdit ? [] : [refsNavigation(schema, onOpenEdit)]),
|
...(schema ? [refsHoverTooltip(schema, colors)] : [])
|
||||||
...(schema ? [refsHoverTooltip(schema, colors, onOpenEdit !== undefined)] : [])
|
|
||||||
],
|
],
|
||||||
[schema, colors, onOpenEdit]
|
[schema, colors]
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleChange(newValue: string) {
|
function handleChange(newValue: string) {
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { Extension } from '@codemirror/state';
|
|
||||||
import { EditorView } from '@uiw/react-codemirror';
|
|
||||||
|
|
||||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
|
||||||
import { findReferenceAt } from '@/utils/codemirror';
|
|
||||||
|
|
||||||
const navigationProducer = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void) => {
|
|
||||||
return EditorView.domEventHandlers({
|
|
||||||
click: (event: MouseEvent, view: EditorView) => {
|
|
||||||
if (!event.ctrlKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });
|
|
||||||
if (!pos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parse = findReferenceAt(pos, view.state);
|
|
||||||
if (!parse || !('entity' in parse.ref)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cst = schema.cstByAlias.get(parse.ref.entity);
|
|
||||||
if (!cst) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
onOpenEdit(cst.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export function refsNavigation(schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void): Extension {
|
|
||||||
return [navigationProducer(schema, onOpenEdit)];
|
|
||||||
}
|
|
|
@ -2,58 +2,65 @@ import { syntaxTree } from '@codemirror/language';
|
||||||
import { Extension } from '@codemirror/state';
|
import { Extension } from '@codemirror/state';
|
||||||
import { hoverTooltip } from '@codemirror/view';
|
import { hoverTooltip } from '@codemirror/view';
|
||||||
|
|
||||||
import { IEntityReference, ISyntacticReference } from '@/models/language';
|
import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI';
|
||||||
import { IRSForm } from '@/models/rsform';
|
import { IRSForm } from '@/models/rsform';
|
||||||
import { IColorTheme } from '@/styling/color';
|
import { IColorTheme } from '@/styling/color';
|
||||||
import {
|
import {
|
||||||
domTooltipEntityReference,
|
domTooltipEntityReference,
|
||||||
domTooltipSyntacticReference,
|
domTooltipSyntacticReference,
|
||||||
findContainedNodes,
|
findContainedNodes,
|
||||||
findReferenceAt
|
findEnvelopingNodes
|
||||||
} from '@/utils/codemirror';
|
} from '@/utils/codemirror';
|
||||||
|
|
||||||
import { RefEntity } from './parse/parser.terms';
|
import { ReferenceTokens } from './parse';
|
||||||
|
import { RefEntity, RefSyntactic } from './parse/parser.terms';
|
||||||
|
|
||||||
export const tooltipProducer = (schema: IRSForm, colors: IColorTheme, canClick?: boolean) => {
|
export const globalsHoverTooltip = (schema: IRSForm, colors: IColorTheme) => {
|
||||||
return hoverTooltip((view, pos) => {
|
return hoverTooltip((view, pos) => {
|
||||||
const parse = findReferenceAt(pos, view.state);
|
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens);
|
||||||
if (!parse) {
|
if (nodes.length !== 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
const start = nodes[0].from;
|
||||||
if ('entity' in parse.ref) {
|
const end = nodes[0].to;
|
||||||
const cst = schema.cstByAlias.get(parse.ref.entity);
|
const text = view.state.doc.sliceString(start, end);
|
||||||
|
if (nodes[0].type.id === RefEntity) {
|
||||||
|
const ref = parseEntityReference(text);
|
||||||
|
const cst = schema.cstByAlias.get(ref.entity);
|
||||||
return {
|
return {
|
||||||
pos: parse.start,
|
pos: start,
|
||||||
end: parse.end,
|
end: end,
|
||||||
above: false,
|
above: false,
|
||||||
create: () => domTooltipEntityReference(parse.ref as IEntityReference, cst, colors, canClick)
|
create: () => domTooltipEntityReference(ref, cst, colors)
|
||||||
};
|
};
|
||||||
} else {
|
} else if (nodes[0].type.id === RefSyntactic) {
|
||||||
|
const ref = parseSyntacticReference(text);
|
||||||
let masterText: string | undefined = undefined;
|
let masterText: string | undefined = undefined;
|
||||||
if (parse.ref.offset > 0) {
|
if (ref.offset > 0) {
|
||||||
const entities = findContainedNodes(parse.end, view.state.doc.length, syntaxTree(view.state), [RefEntity]);
|
const entities = findContainedNodes(end, view.state.doc.length, syntaxTree(view.state), [RefEntity]);
|
||||||
if (parse.ref.offset <= entities.length) {
|
if (ref.offset <= entities.length) {
|
||||||
const master = entities[parse.ref.offset - 1];
|
const master = entities[ref.offset - 1];
|
||||||
masterText = view.state.doc.sliceString(master.from, master.to);
|
masterText = view.state.doc.sliceString(master.from, master.to);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const entities = findContainedNodes(0, parse.start, syntaxTree(view.state), [RefEntity]);
|
const entities = findContainedNodes(0, start, syntaxTree(view.state), [RefEntity]);
|
||||||
if (-parse.ref.offset <= entities.length) {
|
if (-ref.offset <= entities.length) {
|
||||||
const master = entities[-parse.ref.offset - 1];
|
const master = entities[-ref.offset - 1];
|
||||||
masterText = view.state.doc.sliceString(master.from, master.to);
|
masterText = view.state.doc.sliceString(master.from, master.to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pos: parse.start,
|
pos: start,
|
||||||
end: parse.end,
|
end: end,
|
||||||
above: false,
|
above: false,
|
||||||
create: () => domTooltipSyntacticReference(parse.ref as ISyntacticReference, masterText, canClick)
|
create: () => domTooltipSyntacticReference(ref, masterText)
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function refsHoverTooltip(schema: IRSForm, colors: IColorTheme, canClick?: boolean): Extension {
|
export function refsHoverTooltip(schema: IRSForm, colors: IColorTheme): Extension {
|
||||||
return [tooltipProducer(schema, colors, canClick)];
|
return [globalsHoverTooltip(schema, colors)];
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/u
|
||||||
import SearchBar from '@/components/ui/SearchBar';
|
import SearchBar from '@/components/ui/SearchBar';
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useConceptOptions } from '@/context/OptionsContext';
|
import { useConceptOptions } from '@/context/OptionsContext';
|
||||||
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
|
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
||||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
|
|
||||||
import FlexColumn from '../ui/FlexColumn';
|
import FlexColumn from '../ui/FlexColumn';
|
||||||
|
@ -32,8 +32,7 @@ function PickSchema({ id, initialFilter = '', rows = 4, value, onSelectValue }:
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setFilter({
|
setFilter({
|
||||||
query: filterText,
|
query: filterText
|
||||||
type: LibraryItemType.RSFORM
|
|
||||||
});
|
});
|
||||||
}, [filterText]);
|
}, [filterText]);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataCallback,
|
DataCallback,
|
||||||
|
@ -14,8 +14,7 @@ import {
|
||||||
postRSFormFromFile
|
postRSFormFromFile
|
||||||
} from '@/app/backendAPI';
|
} from '@/app/backendAPI';
|
||||||
import { ErrorData } from '@/components/info/InfoError';
|
import { ErrorData } from '@/components/info/InfoError';
|
||||||
import { FolderTree } from '@/models/FolderTree';
|
import { ILibraryItem, LibraryItemID } from '@/models/library';
|
||||||
import { ILibraryItem, LibraryItemID, LocationHead } from '@/models/library';
|
|
||||||
import { ILibraryCreateData } from '@/models/library';
|
import { ILibraryCreateData } from '@/models/library';
|
||||||
import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
|
import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
|
||||||
import { ILibraryFilter } from '@/models/miscellaneous';
|
import { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
|
@ -29,17 +28,10 @@ import { useConceptOptions } from './OptionsContext';
|
||||||
interface ILibraryContext {
|
interface ILibraryContext {
|
||||||
items: ILibraryItem[];
|
items: ILibraryItem[];
|
||||||
templates: ILibraryItem[];
|
templates: ILibraryItem[];
|
||||||
folders: FolderTree;
|
|
||||||
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
loadingError: ErrorData;
|
|
||||||
setLoadingError: (error: ErrorData) => void;
|
|
||||||
|
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
processingError: ErrorData;
|
error: ErrorData;
|
||||||
setProcessingError: (error: ErrorData) => void;
|
setError: (error: ErrorData) => void;
|
||||||
|
|
||||||
reloadItems: (callback?: () => void) => void;
|
|
||||||
|
|
||||||
applyFilter: (params: ILibraryFilter) => ILibraryItem[];
|
applyFilter: (params: ILibraryFilter) => ILibraryItem[];
|
||||||
retrieveTemplate: (templateID: LibraryItemID, callback: (schema: IRSForm) => void) => void;
|
retrieveTemplate: (templateID: LibraryItemID, callback: (schema: IRSForm) => void) => void;
|
||||||
|
@ -72,31 +64,15 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
const [templates, setTemplates] = useState<ILibraryItem[]>([]);
|
const [templates, setTemplates] = useState<ILibraryItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [loadingError, setLoadingError] = useState<ErrorData>(undefined);
|
const [error, setError] = useState<ErrorData>(undefined);
|
||||||
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
|
|
||||||
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
|
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
|
||||||
|
|
||||||
const folders = useMemo(() => {
|
|
||||||
const result = new FolderTree(items.map(item => item.location));
|
|
||||||
result.addPath(LocationHead.USER, 0);
|
|
||||||
result.addPath(LocationHead.COMMON, 0);
|
|
||||||
result.addPath(LocationHead.LIBRARY, 0);
|
|
||||||
result.addPath(LocationHead.PROJECTS, 0);
|
|
||||||
return result;
|
|
||||||
}, [items]);
|
|
||||||
|
|
||||||
const applyFilter = useCallback(
|
const applyFilter = useCallback(
|
||||||
(filter: ILibraryFilter) => {
|
(filter: ILibraryFilter) => {
|
||||||
let result = items;
|
let result = items;
|
||||||
if (!filter.folderMode && filter.head) {
|
if (filter.head) {
|
||||||
result = result.filter(item => item.location.startsWith(filter.head!));
|
result = result.filter(item => item.location.startsWith(filter.head!));
|
||||||
}
|
}
|
||||||
if (filter.folderMode && filter.folder) {
|
|
||||||
result = result.filter(item => item.location == filter.folder);
|
|
||||||
}
|
|
||||||
if (filter.type) {
|
|
||||||
result = result.filter(item => item.item_type === filter.type);
|
|
||||||
}
|
|
||||||
if (filter.isVisible !== undefined) {
|
if (filter.isVisible !== undefined) {
|
||||||
result = result.filter(item => filter.isVisible === item.visible);
|
result = result.filter(item => filter.isVisible === item.visible);
|
||||||
}
|
}
|
||||||
|
@ -109,12 +85,12 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
if (filter.isEditor !== undefined) {
|
if (filter.isEditor !== undefined) {
|
||||||
result = result.filter(item => filter.isEditor == user?.editor.includes(item.id));
|
result = result.filter(item => filter.isEditor == user?.editor.includes(item.id));
|
||||||
}
|
}
|
||||||
if (!filter.folderMode && filter.path) {
|
|
||||||
result = result.filter(item => matchLibraryItemLocation(item, filter.path!));
|
|
||||||
}
|
|
||||||
if (filter.query) {
|
if (filter.query) {
|
||||||
result = result.filter(item => matchLibraryItem(item, filter.query!));
|
result = result.filter(item => matchLibraryItem(item, filter.query!));
|
||||||
}
|
}
|
||||||
|
if (filter.path) {
|
||||||
|
result = result.filter(item => matchLibraryItemLocation(item, filter.path!));
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
[items, user]
|
[items, user]
|
||||||
|
@ -127,11 +103,11 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
callback(cached);
|
callback(cached);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setProcessingError(undefined);
|
setError(undefined);
|
||||||
getRSFormDetails(String(templateID), '', {
|
getRSFormDetails(String(templateID), '', {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setProcessingError,
|
onError: setError,
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
const schema = new RSFormLoader(data).produceRSForm();
|
const schema = new RSFormLoader(data).produceRSForm();
|
||||||
setCachedTemplates(prev => [...prev, schema]);
|
setCachedTemplates(prev => [...prev, schema]);
|
||||||
|
@ -145,12 +121,12 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
const reloadItems = useCallback(
|
const reloadItems = useCallback(
|
||||||
(callback?: () => void) => {
|
(callback?: () => void) => {
|
||||||
setItems([]);
|
setItems([]);
|
||||||
setLoadingError(undefined);
|
setError(undefined);
|
||||||
if (user?.is_staff && adminMode) {
|
if (user?.is_staff && adminMode) {
|
||||||
getAdminLibrary({
|
getAdminLibrary({
|
||||||
setLoading: setLoading,
|
setLoading: setLoading,
|
||||||
showError: true,
|
showError: true,
|
||||||
onError: setLoadingError,
|
onError: setError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setItems(newData);
|
setItems(newData);
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
|
@ -160,7 +136,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
getLibrary({
|
getLibrary({
|
||||||
setLoading: setLoading,
|
setLoading: setLoading,
|
||||||
showError: true,
|
showError: true,
|
||||||
onError: setLoadingError,
|
onError: setError,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setItems(newData);
|
setItems(newData);
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
|
@ -174,8 +150,6 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
const reloadTemplates = useCallback(() => {
|
const reloadTemplates = useCallback(() => {
|
||||||
setTemplates([]);
|
setTemplates([]);
|
||||||
getTemplates({
|
getTemplates({
|
||||||
setLoading: setLoading,
|
|
||||||
onError: setLoadingError,
|
|
||||||
showError: true,
|
showError: true,
|
||||||
onSuccess: newData => setTemplates(newData)
|
onSuccess: newData => setTemplates(newData)
|
||||||
});
|
});
|
||||||
|
@ -216,13 +190,13 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
}
|
}
|
||||||
if (callback) callback(newSchema);
|
if (callback) callback(newSchema);
|
||||||
});
|
});
|
||||||
setProcessingError(undefined);
|
setError(undefined);
|
||||||
if (data.file) {
|
if (data.file) {
|
||||||
postRSFormFromFile({
|
postRSFormFromFile({
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setProcessingError,
|
onError: setError,
|
||||||
onSuccess: onSuccess
|
onSuccess: onSuccess
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -230,7 +204,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setProcessingError,
|
onError: setError,
|
||||||
onSuccess: onSuccess
|
onSuccess: onSuccess
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -240,11 +214,11 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
|
|
||||||
const destroyItem = useCallback(
|
const destroyItem = useCallback(
|
||||||
(target: LibraryItemID, callback?: () => void) => {
|
(target: LibraryItemID, callback?: () => void) => {
|
||||||
setProcessingError(undefined);
|
setError(undefined);
|
||||||
deleteLibraryItem(String(target), {
|
deleteLibraryItem(String(target), {
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setProcessingError,
|
onError: setError,
|
||||||
onSuccess: () =>
|
onSuccess: () =>
|
||||||
reloadItems(() => {
|
reloadItems(() => {
|
||||||
if (user && user.subscriptions.includes(target)) {
|
if (user && user.subscriptions.includes(target)) {
|
||||||
|
@ -257,7 +231,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[reloadItems, user]
|
[setError, reloadItems, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cloneItem = useCallback(
|
const cloneItem = useCallback(
|
||||||
|
@ -265,12 +239,12 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setProcessingError(undefined);
|
setError(undefined);
|
||||||
postCloneLibraryItem(String(target), {
|
postCloneLibraryItem(String(target), {
|
||||||
data: data,
|
data: data,
|
||||||
showError: true,
|
showError: true,
|
||||||
setLoading: setProcessing,
|
setLoading: setProcessing,
|
||||||
onError: setProcessingError,
|
onError: setError,
|
||||||
onSuccess: newSchema =>
|
onSuccess: newSchema =>
|
||||||
reloadItems(() => {
|
reloadItems(() => {
|
||||||
if (user && !user.subscriptions.includes(newSchema.id)) {
|
if (user && !user.subscriptions.includes(newSchema.id)) {
|
||||||
|
@ -280,26 +254,18 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[reloadItems, user]
|
[reloadItems, setError, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryContext.Provider
|
<LibraryContext.Provider
|
||||||
value={{
|
value={{
|
||||||
items,
|
items,
|
||||||
folders,
|
|
||||||
templates,
|
templates,
|
||||||
|
|
||||||
loading,
|
loading,
|
||||||
loadingError,
|
|
||||||
setLoadingError,
|
|
||||||
|
|
||||||
processing,
|
processing,
|
||||||
processingError,
|
error,
|
||||||
setProcessingError,
|
setError,
|
||||||
|
|
||||||
reloadItems,
|
|
||||||
|
|
||||||
applyFilter,
|
applyFilter,
|
||||||
createItem,
|
createItem,
|
||||||
cloneItem,
|
cloneItem,
|
||||||
|
|
|
@ -296,7 +296,8 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
|
||||||
onError: setProcessingError,
|
onError: setProcessingError,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
schema.location = newLocation;
|
schema.location = newLocation;
|
||||||
library.reloadItems(callback);
|
library.localUpdateItem(schema);
|
||||||
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,7 +24,7 @@ interface TemplateTabProps {
|
||||||
|
|
||||||
function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
|
function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
|
||||||
const { templates, retrieveTemplate } = useLibrary();
|
const { templates, retrieveTemplate } = useLibrary();
|
||||||
const [templateSchema, setTemplateSchema] = useState<IRSForm | undefined>(undefined);
|
const [category, setCategory] = useState<IRSForm | undefined>(undefined);
|
||||||
|
|
||||||
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
|
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
|
||||||
|
|
||||||
|
@ -48,16 +48,16 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const categorySelector = useMemo((): { value: number; label: string }[] => {
|
const categorySelector = useMemo((): { value: number; label: string }[] => {
|
||||||
if (!templateSchema) {
|
if (!category) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return templateSchema.items
|
return category.items
|
||||||
.filter(cst => cst.cst_type === CATEGORY_CST_TYPE)
|
.filter(cst => cst.cst_type === CATEGORY_CST_TYPE)
|
||||||
.map(cst => ({
|
.map(cst => ({
|
||||||
value: cst.id,
|
value: cst.id,
|
||||||
label: cst.term_raw
|
label: cst.term_raw
|
||||||
}));
|
}));
|
||||||
}, [templateSchema]);
|
}, [category]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (templates.length > 0 && !state.templateID) {
|
if (templates.length > 0 && !state.templateID) {
|
||||||
|
@ -67,22 +67,22 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!state.templateID) {
|
if (!state.templateID) {
|
||||||
setTemplateSchema(undefined);
|
setCategory(undefined);
|
||||||
} else {
|
} else {
|
||||||
retrieveTemplate(state.templateID, setTemplateSchema);
|
retrieveTemplate(state.templateID, setCategory);
|
||||||
}
|
}
|
||||||
}, [state.templateID, retrieveTemplate]);
|
}, [state.templateID, retrieveTemplate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!templateSchema) {
|
if (!category) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let data = templateSchema.items;
|
let data = category.items;
|
||||||
if (state.filterCategory) {
|
if (state.filterCategory) {
|
||||||
data = applyFilterCategory(state.filterCategory, templateSchema);
|
data = applyFilterCategory(state.filterCategory, category);
|
||||||
}
|
}
|
||||||
setFilteredData(data);
|
setFilteredData(data);
|
||||||
}, [state.filterCategory, templateSchema]);
|
}, [state.filterCategory, category]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimateFade>
|
<AnimateFade>
|
||||||
|
@ -93,16 +93,14 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
|
||||||
className='flex-grow border-none'
|
className='flex-grow border-none'
|
||||||
options={categorySelector}
|
options={categorySelector}
|
||||||
value={
|
value={
|
||||||
state.filterCategory && templateSchema
|
state.filterCategory && category
|
||||||
? {
|
? {
|
||||||
value: state.filterCategory.id,
|
value: state.filterCategory.id,
|
||||||
label: state.filterCategory.term_raw
|
label: state.filterCategory.term_raw
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onChange={data =>
|
onChange={data => partialUpdate({ filterCategory: data ? category?.cstByID.get(data?.value) : undefined })}
|
||||||
partialUpdate({ filterCategory: data ? templateSchema?.cstByID.get(data?.value) : undefined })
|
|
||||||
}
|
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectSingle
|
<SelectSingle
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { FolderTree } from './FolderTree';
|
|
||||||
|
|
||||||
// TODO: test FolderNode and FolderTree exhaustively
|
|
||||||
|
|
||||||
describe('Testing Tree construction', () => {
|
|
||||||
test('empty Tree should be empty', () => {
|
|
||||||
const tree = new FolderTree();
|
|
||||||
expect(tree.roots.size).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('constructing from paths', () => {
|
|
||||||
const tree = new FolderTree(['/S', '/S/project1/123', '/U']);
|
|
||||||
expect(tree.roots.size).toBe(2);
|
|
||||||
expect(tree.roots.get('S')?.children.size).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Testing Tree editing', () => {
|
|
||||||
test('add invalid path', () => {
|
|
||||||
const tree = new FolderTree();
|
|
||||||
expect(() => tree.addPath('invalid')).toThrow(Error);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('add valid path', () => {
|
|
||||||
const tree = new FolderTree();
|
|
||||||
const node = tree.addPath('/S/test');
|
|
||||||
expect(node.getPath()).toBe('/S/test');
|
|
||||||
expect(node.filesInside).toBe(1);
|
|
||||||
expect(node.filesTotal).toBe(1);
|
|
||||||
|
|
||||||
expect(node.parent?.getPath()).toBe('/S');
|
|
||||||
expect(node.parent?.filesInside).toBe(0);
|
|
||||||
expect(node.parent?.filesTotal).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('incrementing counter', () => {
|
|
||||||
const tree = new FolderTree();
|
|
||||||
const node1 = tree.addPath('/S/test', 0);
|
|
||||||
expect(node1.filesInside).toBe(0);
|
|
||||||
expect(node1.filesTotal).toBe(0);
|
|
||||||
|
|
||||||
const node2 = tree.addPath('/S/test', 2);
|
|
||||||
expect(node1).toBe(node2);
|
|
||||||
expect(node2.filesInside).toBe(2);
|
|
||||||
expect(node2.filesTotal).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,157 +0,0 @@
|
||||||
/**
|
|
||||||
* Module: Folder tree data structure. Does not support deletions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents single node of a {@link FolderTree}.
|
|
||||||
*/
|
|
||||||
export class FolderNode {
|
|
||||||
rank: number = 0;
|
|
||||||
text: string;
|
|
||||||
children: Map<string, FolderNode>;
|
|
||||||
parent: FolderNode | undefined;
|
|
||||||
|
|
||||||
filesInside: number = 0;
|
|
||||||
filesTotal: number = 0;
|
|
||||||
|
|
||||||
constructor(text: string, parent?: FolderNode) {
|
|
||||||
this.text = text;
|
|
||||||
this.parent = parent;
|
|
||||||
this.children = new Map();
|
|
||||||
if (parent) {
|
|
||||||
this.rank = parent.rank + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addChild(text: string): FolderNode {
|
|
||||||
const node = new FolderNode(text, this);
|
|
||||||
this.children.set(text, node);
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPredecessor(target: FolderNode): boolean {
|
|
||||||
if (this.parent === target) {
|
|
||||||
return true;
|
|
||||||
} else if (!this.parent) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let node = this.parent;
|
|
||||||
while (node.parent) {
|
|
||||||
if (node.parent === target) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
node = node.parent;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
incrementFiles(count: number = 1): void {
|
|
||||||
this.filesInside = this.filesInside + count;
|
|
||||||
this.incrementTotal(count);
|
|
||||||
}
|
|
||||||
|
|
||||||
incrementTotal(count: number = 1): void {
|
|
||||||
this.filesTotal = this.filesTotal + count;
|
|
||||||
if (this.parent) {
|
|
||||||
this.parent.incrementTotal(count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPath(): string {
|
|
||||||
const suffix = this.text ? `/${this.text}` : '';
|
|
||||||
if (!this.parent) {
|
|
||||||
return suffix;
|
|
||||||
} else {
|
|
||||||
return this.parent.getPath() + suffix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a FolderTree.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export class FolderTree {
|
|
||||||
roots: Map<string, FolderNode> = new Map();
|
|
||||||
|
|
||||||
constructor(arr?: string[]) {
|
|
||||||
arr?.forEach(path => this.addPath(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
at(path: string): FolderNode | undefined {
|
|
||||||
let parse = ChopPathHead(path);
|
|
||||||
if (!this.roots.has(parse.head)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
let node = this.roots.get(parse.head)!;
|
|
||||||
while (parse.tail !== '') {
|
|
||||||
parse = ChopPathHead(parse.tail);
|
|
||||||
if (!node.children.has(parse.head)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
node = node.children.get(parse.head)!;
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTree(): FolderNode[] {
|
|
||||||
const result: FolderNode[] = [];
|
|
||||||
this.roots.forEach(root => this.visitNode(root, result));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private visitNode(target: FolderNode, result: FolderNode[]) {
|
|
||||||
result.push(target);
|
|
||||||
target.children.forEach(child => this.visitNode(child, result));
|
|
||||||
}
|
|
||||||
|
|
||||||
addPath(path: string, filesCount: number = 1): FolderNode {
|
|
||||||
let parse = ChopPathHead(path);
|
|
||||||
if (!parse.head) {
|
|
||||||
throw Error(`Invalid path ${path}`);
|
|
||||||
}
|
|
||||||
let node = this.roots.has(parse.head) ? this.roots.get(parse.head)! : this.addNode(parse.head);
|
|
||||||
while (parse.tail !== '') {
|
|
||||||
parse = ChopPathHead(parse.tail);
|
|
||||||
if (node.children.has(parse.head)) {
|
|
||||||
node = node.children.get(parse.head)!;
|
|
||||||
} else {
|
|
||||||
node = this.addNode(parse.head, node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.incrementFiles(filesCount);
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
private addNode(text: string, parent?: FolderNode): FolderNode {
|
|
||||||
if (parent === undefined) {
|
|
||||||
const newNode = new FolderNode(text);
|
|
||||||
this.roots.set(text, newNode);
|
|
||||||
return newNode;
|
|
||||||
} else {
|
|
||||||
return parent.addChild(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========= Internals =======
|
|
||||||
function ChopPathHead(path: string) {
|
|
||||||
if (!path || path.at(0) !== '/') {
|
|
||||||
return {
|
|
||||||
head: '',
|
|
||||||
tail: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const slash = path.indexOf('/', 1);
|
|
||||||
if (slash === -1) {
|
|
||||||
return {
|
|
||||||
head: path.substring(1),
|
|
||||||
tail: ''
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
head: path.substring(1, slash),
|
|
||||||
tail: path.substring(slash)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -26,9 +26,9 @@ export enum AccessPolicy {
|
||||||
*/
|
*/
|
||||||
export enum LocationHead {
|
export enum LocationHead {
|
||||||
USER = '/U',
|
USER = '/U',
|
||||||
|
LIBRARY = '/L',
|
||||||
COMMON = '/S',
|
COMMON = '/S',
|
||||||
PROJECTS = '/P',
|
PROJECTS = '/P'
|
||||||
LIBRARY = '/L'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
|
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LibraryItemType, LocationHead } from './library';
|
import { LocationHead } from './library';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents graph dependency mode.
|
* Represents graph dependency mode.
|
||||||
|
@ -141,15 +141,10 @@ export enum CstMatchMode {
|
||||||
* Represents Library filter parameters.
|
* Represents Library filter parameters.
|
||||||
*/
|
*/
|
||||||
export interface ILibraryFilter {
|
export interface ILibraryFilter {
|
||||||
type?: LibraryItemType;
|
|
||||||
query?: string;
|
query?: string;
|
||||||
|
|
||||||
path?: string;
|
path?: string;
|
||||||
head?: LocationHead;
|
head?: LocationHead;
|
||||||
|
|
||||||
folderMode?: boolean;
|
|
||||||
folder?: string;
|
|
||||||
|
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isOwned?: boolean;
|
isOwned?: boolean;
|
||||||
isSubscribed?: boolean;
|
isSubscribed?: boolean;
|
||||||
|
|
|
@ -30,7 +30,7 @@ import { information } from '@/utils/labels';
|
||||||
function FormCreateItem() {
|
function FormCreateItem() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { createItem, processingError, setProcessingError, processing } = useLibrary();
|
const { createItem, error, setError, processing } = useLibrary();
|
||||||
|
|
||||||
const [itemType, setItemType] = useState(LibraryItemType.RSFORM);
|
const [itemType, setItemType] = useState(LibraryItemType.RSFORM);
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
|
@ -50,8 +50,8 @@ function FormCreateItem() {
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProcessingError(undefined);
|
setError(undefined);
|
||||||
}, [title, alias, setProcessingError]);
|
}, [title, alias, setError]);
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
if (router.canBack()) {
|
if (router.canBack()) {
|
||||||
|
@ -194,7 +194,7 @@ function FormCreateItem() {
|
||||||
<SubmitButton text='Создать схему' loading={processing} className='min-w-[10rem]' disabled={!isValid} />
|
<SubmitButton text='Создать схему' loading={processing} className='min-w-[10rem]' disabled={!isValid} />
|
||||||
<Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
<Button text='Отмена' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
||||||
</div>
|
</div>
|
||||||
{processingError ? <InfoError error={processingError} /> : null}
|
{error ? <InfoError error={error} /> : null}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { IconFolder, IconFolderClosed, IconFolderOpened, IconFolderTree } from '@/components/Icons';
|
|
||||||
import { CProps } from '@/components/props';
|
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
|
||||||
import { FolderNode, FolderTree } from '@/models/FolderTree';
|
|
||||||
import { animateSideAppear, animateSideView } from '@/styling/animations';
|
|
||||||
import { globals, prefixes } from '@/utils/constants';
|
|
||||||
import { describeFolderNode, labelFolderNode } from '@/utils/labels';
|
|
||||||
|
|
||||||
interface LibraryTableProps {
|
|
||||||
folders: FolderTree;
|
|
||||||
currentFolder: string;
|
|
||||||
setFolder: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
toggleFolderMode: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LibraryFolders({ folders, currentFolder, setFolder, toggleFolderMode }: LibraryTableProps) {
|
|
||||||
const activeNode = useMemo(() => folders.at(currentFolder), [folders, currentFolder]);
|
|
||||||
|
|
||||||
const items = useMemo(() => folders.getTree(), [folders]);
|
|
||||||
const [folded, setFolded] = useState<FolderNode[]>([]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
setFolded(items.filter(item => item !== activeNode && (!activeNode || !activeNode.hasPredecessor(item))));
|
|
||||||
}, [items, activeNode]);
|
|
||||||
|
|
||||||
const onFoldItem = useCallback(
|
|
||||||
(target: FolderNode, showChildren: boolean) => {
|
|
||||||
setFolded(prev =>
|
|
||||||
items.filter(item => {
|
|
||||||
if (item === target) {
|
|
||||||
return !showChildren;
|
|
||||||
}
|
|
||||||
if (!showChildren && item.hasPredecessor(target)) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return prev.includes(item);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[items]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSetValue = useCallback(
|
|
||||||
(event: CProps.EventMouse, target: FolderNode) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
setFolder(target.getPath());
|
|
||||||
},
|
|
||||||
[setFolder]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClickFold = useCallback(
|
|
||||||
(event: CProps.EventMouse, target: FolderNode, showChildren: boolean) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
onFoldItem(target, showChildren);
|
|
||||||
},
|
|
||||||
[onFoldItem]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className='flex flex-col text:xs sm:text-sm'
|
|
||||||
initial={{ ...animateSideView.initial }}
|
|
||||||
animate={{ ...animateSideView.animate }}
|
|
||||||
exit={{ ...animateSideView.exit }}
|
|
||||||
>
|
|
||||||
<div className='h-[2.08rem] flex justify-end pr-1'>
|
|
||||||
<MiniButton
|
|
||||||
icon={<IconFolderTree size='1.25rem' className='icon-green' />}
|
|
||||||
title='Режим: проводник'
|
|
||||||
onClick={toggleFolderMode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'max-w-[10rem] sm:max-w-[15rem] min-w-[10rem] sm:min-w-[15rem]',
|
|
||||||
'flex flex-col',
|
|
||||||
'cc-scroll-y'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{items.map((item, index) =>
|
|
||||||
!item.parent || !folded.includes(item.parent) ? (
|
|
||||||
<motion.div
|
|
||||||
tabIndex={-1}
|
|
||||||
key={`${prefixes.folders_list}${index}`}
|
|
||||||
className={clsx(
|
|
||||||
'min-h-[2.0825rem] sm:min-h-[2.3125rem]',
|
|
||||||
'pr-3 flex items-center gap-2',
|
|
||||||
'cc-scroll-row',
|
|
||||||
'clr-hover',
|
|
||||||
'cursor-pointer',
|
|
||||||
activeNode === item && 'clr-selected'
|
|
||||||
)}
|
|
||||||
style={{ paddingLeft: `${(item.rank > 5 ? 5 : item.rank) * 0.5 + 0.5}rem` }}
|
|
||||||
data-tooltip-id={globals.tooltip}
|
|
||||||
data-tooltip-html={describeFolderNode(item)}
|
|
||||||
onClick={event => handleSetValue(event, item)}
|
|
||||||
initial={{ ...animateSideAppear.initial }}
|
|
||||||
animate={{ ...animateSideAppear.animate }}
|
|
||||||
exit={{ ...animateSideAppear.exit }}
|
|
||||||
>
|
|
||||||
{item.children.size > 0 ? (
|
|
||||||
<MiniButton
|
|
||||||
noPadding
|
|
||||||
noHover
|
|
||||||
icon={
|
|
||||||
folded.includes(item) ? (
|
|
||||||
<IconFolderClosed size='1rem' className='icon-primary' />
|
|
||||||
) : (
|
|
||||||
<IconFolderOpened size='1rem' className='icon-green' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={event => handleClickFold(event, item, folded.includes(item))}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<IconFolder size='1rem' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className='self-center'>{labelFolderNode(item)}</div>
|
|
||||||
</motion.div>
|
|
||||||
) : null
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LibraryFolders;
|
|
|
@ -1,6 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import DataLoader from '@/components/wrap/DataLoader';
|
import DataLoader from '@/components/wrap/DataLoader';
|
||||||
|
@ -12,7 +11,6 @@ import { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
import { storage } from '@/utils/constants';
|
import { storage } from '@/utils/constants';
|
||||||
import { toggleTristateFlag } from '@/utils/utils';
|
import { toggleTristateFlag } from '@/utils/utils';
|
||||||
|
|
||||||
import LibraryFolders from './LibraryFolders';
|
|
||||||
import LibraryTable from './LibraryTable';
|
import LibraryTable from './LibraryTable';
|
||||||
import SearchPanel from './SearchPanel';
|
import SearchPanel from './SearchPanel';
|
||||||
|
|
||||||
|
@ -25,8 +23,6 @@ function LibraryPage() {
|
||||||
const [path, setPath] = useState('');
|
const [path, setPath] = useState('');
|
||||||
|
|
||||||
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
|
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
|
||||||
const [folderMode, setFolderMode] = useLocalStorage<boolean>(storage.librarySearchFolderMode, true);
|
|
||||||
const [folder, setFolder] = useLocalStorage<string>(storage.librarySearchFolder, '');
|
|
||||||
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
|
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
|
||||||
const [isSubscribed, setIsSubscribed] = useLocalStorage<boolean | undefined>(
|
const [isSubscribed, setIsSubscribed] = useLocalStorage<boolean | undefined>(
|
||||||
storage.librarySearchSubscribed,
|
storage.librarySearchSubscribed,
|
||||||
|
@ -43,11 +39,9 @@ function LibraryPage() {
|
||||||
isEditor: user ? isEditor : undefined,
|
isEditor: user ? isEditor : undefined,
|
||||||
isOwned: user ? isOwned : undefined,
|
isOwned: user ? isOwned : undefined,
|
||||||
isSubscribed: user ? isSubscribed : undefined,
|
isSubscribed: user ? isSubscribed : undefined,
|
||||||
isVisible: user ? isVisible : true,
|
isVisible: user ? isVisible : true
|
||||||
folderMode: folderMode,
|
|
||||||
folder: folder
|
|
||||||
}),
|
}),
|
||||||
[head, path, query, isEditor, isOwned, isSubscribed, isVisible, user, folderMode, folder]
|
[head, path, query, isEditor, isOwned, isSubscribed, isVisible, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasCustomFilter = useMemo(
|
const hasCustomFilter = useMemo(
|
||||||
|
@ -58,8 +52,7 @@ function LibraryPage() {
|
||||||
filter.isEditor !== undefined ||
|
filter.isEditor !== undefined ||
|
||||||
filter.isOwned !== undefined ||
|
filter.isOwned !== undefined ||
|
||||||
filter.isSubscribed !== undefined ||
|
filter.isSubscribed !== undefined ||
|
||||||
filter.isVisible !== true ||
|
filter.isVisible !== true,
|
||||||
!!filter.folder,
|
|
||||||
[filter]
|
[filter]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -71,7 +64,6 @@ function LibraryPage() {
|
||||||
const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]);
|
const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]);
|
||||||
const toggleSubscribed = useCallback(() => setIsSubscribed(prev => toggleTristateFlag(prev)), [setIsSubscribed]);
|
const toggleSubscribed = useCallback(() => setIsSubscribed(prev => toggleTristateFlag(prev)), [setIsSubscribed]);
|
||||||
const toggleEditor = useCallback(() => setIsEditor(prev => toggleTristateFlag(prev)), [setIsEditor]);
|
const toggleEditor = useCallback(() => setIsEditor(prev => toggleTristateFlag(prev)), [setIsEditor]);
|
||||||
const toggleFolderMode = useCallback(() => setFolderMode(prev => !prev), [setFolderMode]);
|
|
||||||
|
|
||||||
const resetFilter = useCallback(() => {
|
const resetFilter = useCallback(() => {
|
||||||
setQuery('');
|
setQuery('');
|
||||||
|
@ -81,26 +73,23 @@ function LibraryPage() {
|
||||||
setIsSubscribed(undefined);
|
setIsSubscribed(undefined);
|
||||||
setIsOwned(undefined);
|
setIsOwned(undefined);
|
||||||
setIsEditor(undefined);
|
setIsEditor(undefined);
|
||||||
setFolder('');
|
}, [setHead, setIsVisible, setIsSubscribed, setIsOwned, setIsEditor]);
|
||||||
}, [setHead, setIsVisible, setIsSubscribed, setIsOwned, setIsEditor, setFolder]);
|
|
||||||
|
|
||||||
const view = useMemo(
|
const view = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<LibraryTable
|
<LibraryTable
|
||||||
resetQuery={resetFilter} // prettier: split lines
|
resetQuery={resetFilter} // prettier: split lines
|
||||||
items={items}
|
items={items}
|
||||||
folderMode={folderMode}
|
|
||||||
toggleFolderMode={toggleFolderMode}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[resetFilter, items, folderMode, toggleFolderMode]
|
[resetFilter, items]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataLoader
|
<DataLoader
|
||||||
id='library-page' // prettier: split lines
|
id='library-page' // prettier: split lines
|
||||||
isLoading={library.loading}
|
isLoading={library.loading}
|
||||||
error={library.loadingError}
|
error={library.error}
|
||||||
hasNoData={library.items.length === 0}
|
hasNoData={library.items.length === 0}
|
||||||
>
|
>
|
||||||
<SearchPanel
|
<SearchPanel
|
||||||
|
@ -122,23 +111,8 @@ function LibraryPage() {
|
||||||
isEditor={isEditor}
|
isEditor={isEditor}
|
||||||
toggleEditor={toggleEditor}
|
toggleEditor={toggleEditor}
|
||||||
resetFilter={resetFilter}
|
resetFilter={resetFilter}
|
||||||
folderMode={folderMode}
|
|
||||||
toggleFolderMode={toggleFolderMode}
|
|
||||||
/>
|
/>
|
||||||
|
{view}
|
||||||
<div className='flex'>
|
|
||||||
<AnimatePresence>
|
|
||||||
{folderMode ? (
|
|
||||||
<LibraryFolders
|
|
||||||
currentFolder={folder} // prettier: split-lines
|
|
||||||
setFolder={setFolder}
|
|
||||||
folders={library.folders}
|
|
||||||
toggleFolderMode={toggleFolderMode}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</AnimatePresence>
|
|
||||||
{view}
|
|
||||||
</div>
|
|
||||||
</DataLoader>
|
</DataLoader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
@ -10,7 +9,6 @@ import BadgeLocation from '@/components/info/BadgeLocation';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } from '@/components/ui/DataTable';
|
import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } from '@/components/ui/DataTable';
|
||||||
import FlexColumn from '@/components/ui/FlexColumn';
|
import FlexColumn from '@/components/ui/FlexColumn';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useConceptOptions } from '@/context/OptionsContext';
|
import { useConceptOptions } from '@/context/OptionsContext';
|
||||||
|
@ -23,13 +21,11 @@ import { storage } from '@/utils/constants';
|
||||||
interface LibraryTableProps {
|
interface LibraryTableProps {
|
||||||
items: ILibraryItem[];
|
items: ILibraryItem[];
|
||||||
resetQuery: () => void;
|
resetQuery: () => void;
|
||||||
folderMode: boolean;
|
|
||||||
toggleFolderMode: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<ILibraryItem>();
|
const columnHelper = createColumnHelper<ILibraryItem>();
|
||||||
|
|
||||||
function LibraryTable({ items, resetQuery, folderMode, toggleFolderMode }: LibraryTableProps) {
|
function LibraryTable({ items, resetQuery }: LibraryTableProps) {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getUserLabel } = useUsers();
|
const { getUserLabel } = useUsers();
|
||||||
|
@ -54,42 +50,22 @@ function LibraryTable({ items, resetQuery, folderMode, toggleFolderMode }: Libra
|
||||||
});
|
});
|
||||||
}, [windowSize]);
|
}, [windowSize]);
|
||||||
|
|
||||||
const handleToggleFolder = useCallback(
|
|
||||||
(event: CProps.EventMouse) => {
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
toggleFolderMode();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[toggleFolderMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
...(folderMode
|
columnHelper.accessor('location', {
|
||||||
? []
|
id: 'location',
|
||||||
: [
|
header: () => (
|
||||||
columnHelper.accessor('location', {
|
<div className='pl-2 max-h-[1rem] translate-y-[-0.125rem]'>
|
||||||
id: 'location',
|
<IconFolder size='1.25rem' className='clr-text-controls' />
|
||||||
header: () => (
|
</div>
|
||||||
<MiniButton
|
),
|
||||||
noHover
|
size: 50,
|
||||||
noPadding
|
minSize: 50,
|
||||||
className='pl-2 max-h-[1rem] translate-y-[-0.125rem]'
|
maxSize: 50,
|
||||||
onClick={handleToggleFolder}
|
enableSorting: true,
|
||||||
titleHtml='Ctrl + клик для переключения </br>в режим папок'
|
cell: props => <BadgeLocation location={props.getValue()} />,
|
||||||
icon={<IconFolder size='1.25rem' className='clr-text-controls' />}
|
sortingFn: 'text'
|
||||||
/>
|
}),
|
||||||
),
|
|
||||||
size: 50,
|
|
||||||
minSize: 50,
|
|
||||||
maxSize: 50,
|
|
||||||
enableSorting: true,
|
|
||||||
cell: props => <BadgeLocation location={props.getValue()} />,
|
|
||||||
sortingFn: 'text'
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
columnHelper.accessor('alias', {
|
columnHelper.accessor('alias', {
|
||||||
id: 'alias',
|
id: 'alias',
|
||||||
header: 'Шифр',
|
header: 'Шифр',
|
||||||
|
@ -140,7 +116,7 @@ function LibraryTable({ items, resetQuery, folderMode, toggleFolderMode }: Libra
|
||||||
sortDescFirst: true
|
sortDescFirst: true
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
[intl, getUserLabel, windowSize, handleToggleFolder, folderMode]
|
[intl, getUserLabel, windowSize]
|
||||||
);
|
);
|
||||||
|
|
||||||
const tableHeight = useMemo(() => calculateHeight('2.2rem'), [calculateHeight]);
|
const tableHeight = useMemo(() => calculateHeight('2.2rem'), [calculateHeight]);
|
||||||
|
@ -163,7 +139,7 @@ function LibraryTable({ items, resetQuery, folderMode, toggleFolderMode }: Libra
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
headPosition='0'
|
headPosition='0'
|
||||||
className={clsx('text-xs sm:text-sm cc-scroll-y', { 'border-l border-b': folderMode })}
|
className='text-xs sm:text-sm cc-scroll-y'
|
||||||
style={{ maxHeight: tableHeight }}
|
style={{ maxHeight: tableHeight }}
|
||||||
noDataComponent={
|
noDataComponent={
|
||||||
<FlexColumn className='p-3 items-center min-h-[6rem]'>
|
<FlexColumn className='p-3 items-center min-h-[6rem]'>
|
||||||
|
|
|
@ -4,9 +4,8 @@ import clsx from 'clsx';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { LocationIcon, SubscribeIcon, VisibilityIcon } from '@/components/DomainIcons';
|
import { LocationIcon, SubscribeIcon, VisibilityIcon } from '@/components/DomainIcons';
|
||||||
import { IconEditor, IconFilterReset, IconFolder, IconFolderTree, IconOwner } from '@/components/Icons';
|
import { IconEditor, IconFilterReset, IconFolder, IconOwner } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import { CProps } from '@/components/props';
|
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
import DropdownButton from '@/components/ui/DropdownButton';
|
import DropdownButton from '@/components/ui/DropdownButton';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
@ -33,9 +32,6 @@ interface SearchPanelProps {
|
||||||
head: LocationHead | undefined;
|
head: LocationHead | undefined;
|
||||||
setHead: React.Dispatch<React.SetStateAction<LocationHead | undefined>>;
|
setHead: React.Dispatch<React.SetStateAction<LocationHead | undefined>>;
|
||||||
|
|
||||||
folderMode: boolean;
|
|
||||||
toggleFolderMode: () => void;
|
|
||||||
|
|
||||||
isVisible: boolean | undefined;
|
isVisible: boolean | undefined;
|
||||||
toggleVisible: () => void;
|
toggleVisible: () => void;
|
||||||
isOwned: boolean | undefined;
|
isOwned: boolean | undefined;
|
||||||
|
@ -59,9 +55,6 @@ function SearchPanel({
|
||||||
head,
|
head,
|
||||||
setHead,
|
setHead,
|
||||||
|
|
||||||
folderMode,
|
|
||||||
toggleFolderMode,
|
|
||||||
|
|
||||||
isVisible,
|
isVisible,
|
||||||
toggleVisible,
|
toggleVisible,
|
||||||
isOwned,
|
isOwned,
|
||||||
|
@ -83,22 +76,6 @@ function SearchPanel({
|
||||||
[headMenu, setHead]
|
[headMenu, setHead]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleFolder = useCallback(() => {
|
|
||||||
headMenu.hide();
|
|
||||||
toggleFolderMode();
|
|
||||||
}, [headMenu, toggleFolderMode]);
|
|
||||||
|
|
||||||
const handleFolderClick = useCallback(
|
|
||||||
(event: CProps.EventMouse) => {
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
toggleFolderMode();
|
|
||||||
} else {
|
|
||||||
headMenu.toggle();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[headMenu, toggleFolderMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -157,70 +134,58 @@ function SearchPanel({
|
||||||
value={query}
|
value={query}
|
||||||
onChange={setQuery}
|
onChange={setQuery}
|
||||||
/>
|
/>
|
||||||
{!folderMode ? (
|
|
||||||
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
|
||||||
<SelectorButton
|
|
||||||
transparent
|
|
||||||
className='h-full rounded-lg'
|
|
||||||
titleHtml={(head ? describeLocationHead(head) : 'Выберите каталог') + '<br/>Ctrl + клик - Проводник'}
|
|
||||||
hideTitle={headMenu.isOpen}
|
|
||||||
icon={
|
|
||||||
head ? (
|
|
||||||
<LocationIcon value={head} size='1.25rem' />
|
|
||||||
) : (
|
|
||||||
<IconFolder size='1.25rem' className='clr-text-controls' />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={handleFolderClick}
|
|
||||||
text={head ?? '//'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown isOpen={headMenu.isOpen} stretchLeft className='z-modalTooltip'>
|
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
||||||
<DropdownButton className='w-[10rem]' onClick={() => handleChange(undefined)}>
|
<SelectorButton
|
||||||
<div className='inline-flex items-center gap-3'>
|
transparent
|
||||||
<IconFolder size='1rem' className='clr-text-controls' />
|
className='h-full rounded-lg'
|
||||||
<span>отображать все</span>
|
title={head ? describeLocationHead(head) : 'Выберите каталог'}
|
||||||
</div>
|
hideTitle={headMenu.isOpen}
|
||||||
</DropdownButton>
|
icon={
|
||||||
{Object.values(LocationHead).map((head, index) => {
|
head ? (
|
||||||
return (
|
<LocationIcon value={head} size='1.25rem' />
|
||||||
<DropdownButton
|
) : (
|
||||||
className='w-[10rem]'
|
<IconFolder size='1.25rem' className='clr-text-controls' />
|
||||||
key={`${prefixes.location_head_list}${index}`}
|
)
|
||||||
onClick={() => handleChange(head)}
|
}
|
||||||
title={describeLocationHead(head)}
|
onClick={headMenu.toggle}
|
||||||
>
|
text={head ?? '//'}
|
||||||
<div className='inline-flex items-center gap-3'>
|
|
||||||
<LocationIcon value={head} size='1rem' />
|
|
||||||
{labelLocationHead(head)}
|
|
||||||
</div>
|
|
||||||
</DropdownButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<DropdownButton
|
|
||||||
className='w-[10rem]'
|
|
||||||
title='переключение в режим выбора папок'
|
|
||||||
onClick={handleToggleFolder}
|
|
||||||
>
|
|
||||||
<div className='inline-flex items-center gap-3'>
|
|
||||||
<IconFolderTree size='1rem' className='clr-text-controls' />
|
|
||||||
<span>проводник...</span>
|
|
||||||
</div>
|
|
||||||
</DropdownButton>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{!folderMode ? (
|
|
||||||
<SearchBar
|
|
||||||
id='path_search'
|
|
||||||
placeholder='Путь'
|
|
||||||
noIcon
|
|
||||||
noBorder
|
|
||||||
className='min-w-[4.5rem] sm:min-w-[5rem]'
|
|
||||||
value={path}
|
|
||||||
onChange={setPath}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
|
<Dropdown isOpen={headMenu.isOpen} stretchLeft className='z-modalTooltip'>
|
||||||
|
<DropdownButton className='w-[10rem]' onClick={() => handleChange(undefined)}>
|
||||||
|
<div className='inline-flex items-center gap-3'>
|
||||||
|
<IconFolder size='1rem' className='clr-text-controls' />
|
||||||
|
<span>отображать все</span>
|
||||||
|
</div>
|
||||||
|
</DropdownButton>
|
||||||
|
{Object.values(LocationHead).map((head, index) => {
|
||||||
|
return (
|
||||||
|
<DropdownButton
|
||||||
|
className='w-[10rem]'
|
||||||
|
key={`${prefixes.location_head_list}${index}`}
|
||||||
|
onClick={() => handleChange(head)}
|
||||||
|
title={describeLocationHead(head)}
|
||||||
|
>
|
||||||
|
<div className='inline-flex items-center gap-3'>
|
||||||
|
<LocationIcon value={head} size='1rem' />
|
||||||
|
{labelLocationHead(head)}
|
||||||
|
</div>
|
||||||
|
</DropdownButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchBar
|
||||||
|
id='path_search'
|
||||||
|
placeholder='Путь'
|
||||||
|
noIcon
|
||||||
|
noBorder
|
||||||
|
className='min-w-[4.5rem] sm:min-w-[5rem]'
|
||||||
|
value={path}
|
||||||
|
onChange={setPath}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Overlay position='top-[-0.75rem] right-0'>
|
<Overlay position='top-[-0.75rem] right-0'>
|
||||||
<BadgeHelp
|
<BadgeHelp
|
||||||
|
|
|
@ -146,7 +146,6 @@ function FormConstituenta({
|
||||||
maxHeight='8rem'
|
maxHeight='8rem'
|
||||||
placeholder='Обозначение, используемое в текстовых определениях'
|
placeholder='Обозначение, используемое в текстовых определениях'
|
||||||
schema={schema}
|
schema={schema}
|
||||||
onOpenEdit={onOpenEdit}
|
|
||||||
value={term}
|
value={term}
|
||||||
initialValue={state?.term_raw ?? ''}
|
initialValue={state?.term_raw ?? ''}
|
||||||
resolved={state?.term_resolved ?? ''}
|
resolved={state?.term_resolved ?? ''}
|
||||||
|
@ -197,7 +196,6 @@ function FormConstituenta({
|
||||||
minHeight='3.75rem'
|
minHeight='3.75rem'
|
||||||
maxHeight='8rem'
|
maxHeight='8rem'
|
||||||
schema={schema}
|
schema={schema}
|
||||||
onOpenEdit={onOpenEdit}
|
|
||||||
value={textDefinition}
|
value={textDefinition}
|
||||||
initialValue={state?.definition_raw ?? ''}
|
initialValue={state?.definition_raw ?? ''}
|
||||||
resolved={state?.definition_resolved ?? ''}
|
resolved={state?.definition_resolved ?? ''}
|
||||||
|
|
|
@ -59,7 +59,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
) : null}
|
) : null}
|
||||||
<LabeledValue className='max-w-[30rem] sm:mb-1 text-ellipsis' label='Путь' text={item?.location ?? ''} />
|
<LabeledValue className='sm:mb-1 text-ellipsis' label='Путь' text={item?.location ?? ''} />
|
||||||
|
|
||||||
{accessLevel >= UserLevel.OWNER ? (
|
{accessLevel >= UserLevel.OWNER ? (
|
||||||
<Overlay position='top-[-0.5rem] left-[5.5rem] cc-icons'>
|
<Overlay position='top-[-0.5rem] left-[5.5rem] cc-icons'>
|
||||||
|
|
|
@ -54,7 +54,7 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
|
||||||
onKeyDown={handleInput}
|
onKeyDown={handleInput}
|
||||||
className={clsx('sm:w-fit sm:max-w-fit max-w-[32rem]', 'mx-auto ', 'flex flex-col sm:flex-row px-6')}
|
className={clsx('sm:w-fit sm:max-w-fit max-w-[32rem]', 'mx-auto ', 'flex flex-col sm:flex-row px-6')}
|
||||||
>
|
>
|
||||||
<FlexColumn className='flex-shrink'>
|
<FlexColumn>
|
||||||
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
||||||
<EditorLibraryItem item={schema} isModified={isModified} controller={controller} />
|
<EditorLibraryItem item={schema} isModified={isModified} controller={controller} />
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
|
|
|
@ -6,11 +6,9 @@ import { NodeType, Tree, TreeCursor } from '@lezer/common';
|
||||||
import { EditorState, ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
import { EditorState, ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { ReferenceTokens } from '@/components/RefsInput/parse';
|
|
||||||
import { RefEntity } from '@/components/RefsInput/parse/parser.terms';
|
|
||||||
import { GlobalTokens } from '@/components/RSInput/rslang';
|
import { GlobalTokens } from '@/components/RSInput/rslang';
|
||||||
import { IEntityReference, ISyntacticReference } from '@/models/language';
|
import { IEntityReference, ISyntacticReference } from '@/models/language';
|
||||||
import { parseEntityReference, parseGrammemes, parseSyntacticReference } from '@/models/languageAPI';
|
import { parseGrammemes } from '@/models/languageAPI';
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
import { isBasicConcept } from '@/models/rsformAPI';
|
import { isBasicConcept } from '@/models/rsformAPI';
|
||||||
|
|
||||||
|
@ -144,30 +142,13 @@ export function findAliasAt(pos: number, state: EditorState) {
|
||||||
return { alias, start, end };
|
return { alias, start, end };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves reference from position in Editor.
|
|
||||||
*/
|
|
||||||
export function findReferenceAt(pos: number, state: EditorState) {
|
|
||||||
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), ReferenceTokens);
|
|
||||||
if (nodes.length !== 1) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const start = nodes[0].from;
|
|
||||||
const end = nodes[0].to;
|
|
||||||
const text = state.doc.sliceString(start, end);
|
|
||||||
if (nodes[0].type.id === RefEntity) {
|
|
||||||
return { ref: parseEntityReference(text), start, end };
|
|
||||||
} else {
|
|
||||||
return { ref: parseSyntacticReference(text), start, end };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create DOM tooltip for {@link Constituenta}.
|
* Create DOM tooltip for {@link Constituenta}.
|
||||||
*/
|
*/
|
||||||
export function domTooltipConstituenta(cst?: IConstituenta, canClick?: boolean) {
|
export function domTooltipConstituenta(cst?: IConstituenta) {
|
||||||
const dom = document.createElement('div');
|
const dom = document.createElement('div');
|
||||||
dom.className = clsx(
|
dom.className = clsx(
|
||||||
|
'z-modalTooltip',
|
||||||
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
||||||
'dense',
|
'dense',
|
||||||
'p-2',
|
'p-2',
|
||||||
|
@ -225,12 +206,10 @@ export function domTooltipConstituenta(cst?: IConstituenta, canClick?: boolean)
|
||||||
dom.appendChild(children);
|
dom.appendChild(children);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canClick) {
|
const clickTip = document.createElement('p');
|
||||||
const clickTip = document.createElement('p');
|
clickTip.className = 'w-full text-center text-xs mt-2';
|
||||||
clickTip.className = 'w-full text-center text-xs mt-2';
|
clickTip.innerText = 'Ctrl + клик для перехода';
|
||||||
clickTip.innerText = 'Ctrl + клик для перехода';
|
dom.appendChild(clickTip);
|
||||||
dom.appendChild(clickTip);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { dom: dom };
|
return { dom: dom };
|
||||||
}
|
}
|
||||||
|
@ -238,14 +217,10 @@ export function domTooltipConstituenta(cst?: IConstituenta, canClick?: boolean)
|
||||||
/**
|
/**
|
||||||
* Create DOM tooltip for {@link IEntityReference}.
|
* Create DOM tooltip for {@link IEntityReference}.
|
||||||
*/
|
*/
|
||||||
export function domTooltipEntityReference(
|
export function domTooltipEntityReference(ref: IEntityReference, cst: IConstituenta | undefined, colors: IColorTheme) {
|
||||||
ref: IEntityReference,
|
|
||||||
cst: IConstituenta | undefined,
|
|
||||||
colors: IColorTheme,
|
|
||||||
canClick?: boolean
|
|
||||||
) {
|
|
||||||
const dom = document.createElement('div');
|
const dom = document.createElement('div');
|
||||||
dom.className = clsx(
|
dom.className = clsx(
|
||||||
|
'z-tooltip',
|
||||||
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
||||||
'dense',
|
'dense',
|
||||||
'p-2 flex flex-col',
|
'p-2 flex flex-col',
|
||||||
|
@ -283,27 +258,16 @@ export function domTooltipEntityReference(
|
||||||
grams.appendChild(gram);
|
grams.appendChild(gram);
|
||||||
});
|
});
|
||||||
dom.appendChild(grams);
|
dom.appendChild(grams);
|
||||||
|
|
||||||
if (canClick) {
|
|
||||||
const clickTip = document.createElement('p');
|
|
||||||
clickTip.className = 'w-full text-center text-xs mt-2';
|
|
||||||
clickTip.innerHTML = 'Ctrl + клик для перехода</br>Ctrl + пробел для редактирования';
|
|
||||||
dom.appendChild(clickTip);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { dom: dom };
|
return { dom: dom };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create DOM tooltip for {@link ISyntacticReference}.
|
* Create DOM tooltip for {@link ISyntacticReference}.
|
||||||
*/
|
*/
|
||||||
export function domTooltipSyntacticReference(
|
export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef: string | undefined) {
|
||||||
ref: ISyntacticReference,
|
|
||||||
masterRef: string | undefined,
|
|
||||||
canClick?: boolean
|
|
||||||
) {
|
|
||||||
const dom = document.createElement('div');
|
const dom = document.createElement('div');
|
||||||
dom.className = clsx(
|
dom.className = clsx(
|
||||||
|
'z-tooltip',
|
||||||
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
|
||||||
'dense',
|
'dense',
|
||||||
'p-2 flex flex-col',
|
'p-2 flex flex-col',
|
||||||
|
@ -329,13 +293,6 @@ export function domTooltipSyntacticReference(
|
||||||
nominal.innerHTML = `<b>Начальная форма:</b> ${ref.nominal}`;
|
nominal.innerHTML = `<b>Начальная форма:</b> ${ref.nominal}`;
|
||||||
dom.appendChild(nominal);
|
dom.appendChild(nominal);
|
||||||
|
|
||||||
if (canClick) {
|
|
||||||
const clickTip = document.createElement('p');
|
|
||||||
clickTip.className = 'w-full text-center text-xs mt-2';
|
|
||||||
clickTip.innerHTML = 'Ctrl + пробел для редактирования';
|
|
||||||
dom.appendChild(clickTip);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { dom: dom };
|
return { dom: dom };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,8 +94,6 @@ export const storage = {
|
||||||
rseditShowControls: 'rsedit.show_controls',
|
rseditShowControls: 'rsedit.show_controls',
|
||||||
|
|
||||||
librarySearchHead: 'library.search.head',
|
librarySearchHead: 'library.search.head',
|
||||||
librarySearchFolderMode: 'library.search.folder_mode',
|
|
||||||
librarySearchFolder: 'library.search.folder',
|
|
||||||
librarySearchVisible: 'library.search.visible',
|
librarySearchVisible: 'library.search.visible',
|
||||||
librarySearchOwned: 'library.search.owned',
|
librarySearchOwned: 'library.search.owned',
|
||||||
librarySearchSubscribed: 'library.search.subscribed',
|
librarySearchSubscribed: 'library.search.subscribed',
|
||||||
|
@ -146,7 +144,6 @@ export const prefixes = {
|
||||||
policy_list: 'policy_list_',
|
policy_list: 'policy_list_',
|
||||||
library_filters_list: 'library_filters_list_',
|
library_filters_list: 'library_filters_list_',
|
||||||
location_head_list: 'location_head_list_',
|
location_head_list: 'location_head_list_',
|
||||||
folders_list: 'folders_list_',
|
|
||||||
topic_list: 'topic_list_',
|
topic_list: 'topic_list_',
|
||||||
topic_item: 'topic_item_',
|
topic_item: 'topic_item_',
|
||||||
library_list: 'library_list_',
|
library_list: 'library_list_',
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
* Description is a long description used in tooltips.
|
* Description is a long description used in tooltips.
|
||||||
*/
|
*/
|
||||||
import { GraphLayout } from '@/components/ui/GraphUI';
|
import { GraphLayout } from '@/components/ui/GraphUI';
|
||||||
import { FolderNode } from '@/models/FolderTree';
|
|
||||||
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
||||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||||
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
||||||
|
@ -821,20 +820,6 @@ export function describeAccessMode(mode: UserLevel): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves label for {@link FolderNode}.
|
|
||||||
*/
|
|
||||||
export function labelFolderNode(node: FolderNode): string {
|
|
||||||
return node.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves description for {@link FolderNode}.
|
|
||||||
*/
|
|
||||||
export function describeFolderNode(node: FolderNode): string {
|
|
||||||
return `${node.filesInside} | ${node.filesTotal}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves label for {@link AccessPolicy}.
|
* Retrieves label for {@link AccessPolicy}.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue
Block a user