Compare commits
8 Commits
2b29f0b7bf
...
b03e2033eb
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b03e2033eb | ||
![]() |
71659b8c15 | ||
![]() |
4172e387c2 | ||
![]() |
c7df031041 | ||
![]() |
e8c3106563 | ||
![]() |
a1bfa8a119 | ||
![]() |
3f398bd700 | ||
![]() |
03e6bc55d9 |
3
.github/workflows/frontend.yml
vendored
3
.github/workflows/frontend.yml
vendored
|
@ -38,6 +38,7 @@ jobs:
|
|||
npm install -g typescript vite jest
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
- name: Test
|
||||
- name: Run CI
|
||||
run: |
|
||||
npm run lint
|
||||
npm test
|
||||
|
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -77,6 +77,8 @@
|
|||
"csrftoken",
|
||||
"cstlist",
|
||||
"csttype",
|
||||
"dagre",
|
||||
"dagrejs",
|
||||
"datv",
|
||||
"Debool",
|
||||
"Decart",
|
||||
|
@ -91,6 +93,7 @@
|
|||
"Geologica",
|
||||
"Grammeme",
|
||||
"Grammemes",
|
||||
"graphlib",
|
||||
"GRND",
|
||||
"IDEF",
|
||||
"impr",
|
||||
|
@ -108,6 +111,7 @@
|
|||
"multiword",
|
||||
"mypy",
|
||||
"nocheck",
|
||||
"nodesep",
|
||||
"nomn",
|
||||
"nooverlap",
|
||||
"NPRO",
|
||||
|
@ -127,6 +131,8 @@
|
|||
"pylint",
|
||||
"pymorphy",
|
||||
"Quantor",
|
||||
"rankdir",
|
||||
"ranksep",
|
||||
"razdel",
|
||||
"reactflow",
|
||||
"reagraph",
|
||||
|
|
|
@ -50,6 +50,7 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
- @uiw/react-codemirror
|
||||
- @uiw/codemirror-themes
|
||||
- @lezer/lr
|
||||
- @dagrejs/dagre
|
||||
</pre>
|
||||
</details>
|
||||
<details>
|
||||
|
|
540
rsconcept/frontend/package-lock.json
generated
540
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -12,6 +12,7 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@uiw/codemirror-themes": "^4.23.6",
|
||||
|
@ -26,7 +27,7 @@
|
|||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-intl": "^6.8.7",
|
||||
"react-intl": "^6.8.9",
|
||||
"react-loader-spinner": "^6.1.6",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-select": "^5.8.3",
|
||||
|
@ -41,14 +42,14 @@
|
|||
"devDependencies": {
|
||||
"@lezer/generator": "^1.7.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/node": "^22.9.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^15.12.0",
|
||||
|
@ -57,7 +58,7 @@
|
|||
"tailwindcss": "^3.4.15",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.14.0",
|
||||
"typescript-eslint": "^8.15.0",
|
||||
"vite": "^5.4.11"
|
||||
},
|
||||
"jest": {
|
||||
|
|
|
@ -24,7 +24,7 @@ function UserMenu() {
|
|||
<AnimatePresence mode='wait'>
|
||||
{loading ? (
|
||||
<AnimateFade key='nav_user_badge_loader'>
|
||||
<Loader circular size={3} />
|
||||
<Loader circular scale={1.5} />
|
||||
</AnimateFade>
|
||||
) : null}
|
||||
{!user && !loading ? (
|
||||
|
|
|
@ -45,6 +45,7 @@ export interface DomIconProps<RequestData> extends IconProps {
|
|||
value: RequestData;
|
||||
}
|
||||
|
||||
/** Icon for library item type. */
|
||||
export function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps<LibraryItemType>) {
|
||||
switch (value) {
|
||||
case LibraryItemType.RSFORM:
|
||||
|
@ -54,6 +55,7 @@ export function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProp
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for access policy. */
|
||||
export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<AccessPolicy>) {
|
||||
switch (value) {
|
||||
case AccessPolicy.PRIVATE:
|
||||
|
@ -65,6 +67,7 @@ export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps<
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for visibility. */
|
||||
export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||
if (value) {
|
||||
return <IconShow size={size} className={className ?? 'clr-text-green'} />;
|
||||
|
@ -73,6 +76,7 @@ export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconPr
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for subfolders. */
|
||||
export function SubfoldersIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||
if (value) {
|
||||
return <IconSubfolders size={size} className={className ?? 'clr-text-green'} />;
|
||||
|
@ -81,6 +85,7 @@ export function SubfoldersIcon({ value, size = '1.25rem', className }: DomIconPr
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for location. */
|
||||
export function LocationIcon({ value, size = '1.25rem', className }: DomIconProps<string>) {
|
||||
switch (value.substring(0, 2) as LocationHead) {
|
||||
case LocationHead.COMMON:
|
||||
|
@ -94,6 +99,7 @@ export function LocationIcon({ value, size = '1.25rem', className }: DomIconProp
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for term graph dependency mode. */
|
||||
export function DependencyIcon({ value, size = '1.25rem', className }: DomIconProps<DependencyMode>) {
|
||||
switch (value) {
|
||||
case DependencyMode.ALL:
|
||||
|
@ -109,6 +115,7 @@ export function DependencyIcon({ value, size = '1.25rem', className }: DomIconPr
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for constituenta match mode. */
|
||||
export function MatchModeIcon({ value, size = '1.25rem', className }: DomIconProps<CstMatchMode>) {
|
||||
switch (value) {
|
||||
case CstMatchMode.ALL:
|
||||
|
@ -124,6 +131,7 @@ export function MatchModeIcon({ value, size = '1.25rem', className }: DomIconPro
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for expression status. */
|
||||
export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<ExpressionStatus>) {
|
||||
switch (value) {
|
||||
case ExpressionStatus.VERIFIED:
|
||||
|
@ -141,6 +149,7 @@ export function StatusIcon({ value, size = '1.25rem', className }: DomIconProps<
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for constituenta type. */
|
||||
export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps<CstType>) {
|
||||
switch (value) {
|
||||
case CstType.BASE:
|
||||
|
@ -162,6 +171,7 @@ export function CstTypeIcon({ value, size = '1.25rem', className }: DomIconProps
|
|||
}
|
||||
}
|
||||
|
||||
/** Icon for relocation direction. */
|
||||
export function RelocateUpIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
|
||||
if (value) {
|
||||
return <IconMoveUp size={size} className={className ?? 'clr-text-primary'} />;
|
||||
|
|
|
@ -7,11 +7,19 @@ import { CProps } from '../props';
|
|||
import TooltipConstituenta from './TooltipConstituenta';
|
||||
|
||||
interface BadgeConstituentaProps extends CProps.Styling {
|
||||
/** Prefix for tooltip ID. */
|
||||
prefixID?: string;
|
||||
|
||||
/** Constituenta to display. */
|
||||
value: IConstituenta;
|
||||
|
||||
/** Color theme to use. */
|
||||
theme: IColorTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a badge with a constituenta alias and information tooltip.
|
||||
*/
|
||||
function BadgeConstituenta({ value, prefixID, className, style, theme }: BadgeConstituentaProps) {
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -6,9 +6,13 @@ import { colorFgGrammeme } from '@/styling/color';
|
|||
import { labelGrammeme } from '@/utils/labels';
|
||||
|
||||
interface BadgeGrammemeProps {
|
||||
/** Grammeme to display. */
|
||||
grammeme: GramData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a badge with a grammeme tag.
|
||||
*/
|
||||
function BadgeGrammeme({ grammeme }: BadgeGrammemeProps) {
|
||||
const { colors } = useConceptOptions();
|
||||
return (
|
||||
|
|
|
@ -8,12 +8,22 @@ import { IconHelp } from '../Icons';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface BadgeHelpProps extends CProps.Styling {
|
||||
/** Topic to display in a tooltip. */
|
||||
topic: HelpTopic;
|
||||
|
||||
/** Offset from the cursor to the tooltip. */
|
||||
offset?: number;
|
||||
|
||||
/** Classname for padding. */
|
||||
padding?: string;
|
||||
|
||||
/** Place of the tooltip in relation to the cursor. */
|
||||
place?: PlacesType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display help icon with a manual page tooltip.
|
||||
*/
|
||||
function BadgeHelp({ topic, padding = 'p-1', ...restProps }: BadgeHelpProps) {
|
||||
const { showHelp } = useConceptOptions();
|
||||
|
||||
|
|
|
@ -3,9 +3,13 @@ import { globals } from '@/utils/constants';
|
|||
import { LocationIcon } from '../DomainIcons';
|
||||
|
||||
interface BadgeLocationProps {
|
||||
/** Location to display. */
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays location icon with a full text tooltip.
|
||||
*/
|
||||
function BadgeLocation({ location }: BadgeLocationProps) {
|
||||
return (
|
||||
<div className='pl-2' data-tooltip-id={globals.tooltip} data-tooltip-content={location}>
|
||||
|
|
|
@ -3,10 +3,16 @@ import { IWordForm } from '@/models/language';
|
|||
import BadgeGrammeme from './BadgeGrammeme';
|
||||
|
||||
interface BadgeWordFormProps {
|
||||
keyPrefix?: string;
|
||||
/** Word form to display. */
|
||||
form: IWordForm;
|
||||
|
||||
/** Prefix for grammemes keys. */
|
||||
keyPrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a badge with grammemes of a word form.
|
||||
*/
|
||||
function BadgeWordForm({ keyPrefix, form }: BadgeWordFormProps) {
|
||||
return (
|
||||
<div className='flex flex-wrap justify-start gap-1 select-none w-fit'>
|
||||
|
|
|
@ -98,8 +98,8 @@ function PickConstituenta({
|
|||
id={id ? `${id}__search` : undefined}
|
||||
className='clr-input rounded-t-md'
|
||||
noBorder
|
||||
value={filterText}
|
||||
onChange={newValue => setFilterText(newValue)}
|
||||
query={filterText}
|
||||
onChangeQuery={newValue => setFilterText(newValue)}
|
||||
/>
|
||||
<DataTable
|
||||
id={id}
|
||||
|
|
|
@ -132,8 +132,8 @@ function PickMultiConstituenta({
|
|||
id='dlg_constituents_search'
|
||||
noBorder
|
||||
className='min-w-[6rem] pr-2 flex-grow'
|
||||
value={filterText}
|
||||
onChange={setFilterText}
|
||||
query={filterText}
|
||||
onChangeQuery={setFilterText}
|
||||
/>
|
||||
<ToolbarGraphSelection
|
||||
graph={foldedGraph}
|
||||
|
|
|
@ -129,8 +129,8 @@ function PickSchema({
|
|||
id={id ? `${id}__search` : undefined}
|
||||
className='clr-input flex-grow rounded-t-md'
|
||||
noBorder
|
||||
value={filterText}
|
||||
onChange={newValue => setFilterText(newValue)}
|
||||
query={filterText}
|
||||
onChangeQuery={newValue => setFilterText(newValue)}
|
||||
/>
|
||||
<div ref={locationMenu.ref}>
|
||||
<MiniButton
|
||||
|
|
|
@ -11,11 +11,11 @@ interface SelectMultiGrammemeProps
|
|||
extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'>,
|
||||
CProps.Styling {
|
||||
value: IGrammemeOption[];
|
||||
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
|
||||
onChangeValue: (newValue: IGrammemeOption[]) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function SelectMultiGrammeme({ value, setValue, ...restProps }: SelectMultiGrammemeProps) {
|
||||
function SelectMultiGrammeme({ value, onChangeValue, ...restProps }: SelectMultiGrammemeProps) {
|
||||
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -29,7 +29,7 @@ function SelectMultiGrammeme({ value, setValue, ...restProps }: SelectMultiGramm
|
|||
<SelectMulti
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))}
|
||||
onChange={newValue => onChangeValue([...newValue].sort(compareGrammemeOptions))}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -25,45 +25,84 @@ import TableHeader from './TableHeader';
|
|||
|
||||
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
|
||||
|
||||
/** Style to conditionally apply to rows. */
|
||||
export interface IConditionalStyle<TData> {
|
||||
/** Callback to determine if the style should be applied. */
|
||||
when: (rowData: TData) => boolean;
|
||||
|
||||
/** Style to apply. */
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface DataTableProps<TData extends RowData>
|
||||
extends CProps.Styling,
|
||||
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
|
||||
/** Id of the component. */
|
||||
id?: string;
|
||||
|
||||
/** Indicates that padding should be minimal. */
|
||||
dense?: boolean;
|
||||
|
||||
/** Number of rows to display. */
|
||||
rows?: number;
|
||||
|
||||
/** Height of the content. */
|
||||
contentHeight?: string;
|
||||
|
||||
/** Top position of sticky header (0 if no other sticky elements are present). */
|
||||
headPosition?: string;
|
||||
|
||||
/** Disable header. */
|
||||
noHeader?: boolean;
|
||||
|
||||
/** Disable footer. */
|
||||
noFooter?: boolean;
|
||||
|
||||
/** List of styles to conditionally apply to rows. */
|
||||
conditionalRowStyles?: IConditionalStyle<TData>[];
|
||||
|
||||
/** Component to display when there is no data. */
|
||||
noDataComponent?: React.ReactNode;
|
||||
|
||||
/** Callback to be called when a row is clicked. */
|
||||
onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void;
|
||||
|
||||
/** Callback to be called when a row is double clicked. */
|
||||
onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void;
|
||||
|
||||
/** Enable row selection. */
|
||||
enableRowSelection?: boolean;
|
||||
|
||||
/** Current row selection. */
|
||||
rowSelection?: RowSelectionState;
|
||||
|
||||
/** Enable hiding of columns. */
|
||||
enableHiding?: boolean;
|
||||
|
||||
/** Current column visibility. */
|
||||
columnVisibility?: VisibilityState;
|
||||
|
||||
/** Enable pagination. */
|
||||
enablePagination?: boolean;
|
||||
|
||||
/** Number of rows per page. */
|
||||
paginationPerPage?: number;
|
||||
|
||||
/** List of options to choose from for pagination. */
|
||||
paginationOptions?: number[];
|
||||
|
||||
/** Callback to be called when the pagination option is changed. */
|
||||
onChangePaginationOption?: (newValue: number) => void;
|
||||
|
||||
/** Enable sorting. */
|
||||
enableSorting?: boolean;
|
||||
|
||||
/** Initial sorting. */
|
||||
initialSorting?: ColumnSort;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI element: data representation as a table.
|
||||
* Dta representation as a table.
|
||||
*
|
||||
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
|
||||
* No sticky header if omitted
|
||||
|
@ -157,7 +196,7 @@ function DataTable<TData extends RowData>({
|
|||
enableRowSelection={enableRowSelection}
|
||||
enableSorting={enableSorting}
|
||||
headPosition={headPosition}
|
||||
setLastSelected={setLastSelected}
|
||||
resetLastSelected={() => setLastSelected(undefined)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
@ -168,7 +207,7 @@ function DataTable<TData extends RowData>({
|
|||
conditionalRowStyles={conditionalRowStyles}
|
||||
enableRowSelection={enableRowSelection}
|
||||
lastSelected={lastSelected}
|
||||
setLastSelected={setLastSelected}
|
||||
onChangeLastSelected={setLastSelected}
|
||||
onRowClicked={onRowClicked}
|
||||
onRowDoubleClicked={onRowDoubleClicked}
|
||||
/>
|
||||
|
|
|
@ -4,12 +4,12 @@ import CheckboxTristate from '@/components/ui/CheckboxTristate';
|
|||
|
||||
interface SelectAllProps<TData> {
|
||||
table: Table<TData>;
|
||||
setLastSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
resetLastSelected: () => void;
|
||||
}
|
||||
|
||||
function SelectAll<TData>({ table, setLastSelected }: SelectAllProps<TData>) {
|
||||
function SelectAll<TData>({ table, resetLastSelected }: SelectAllProps<TData>) {
|
||||
function handleChange(value: boolean | null) {
|
||||
setLastSelected(undefined);
|
||||
resetLastSelected();
|
||||
table.toggleAllPageRowsSelected(value !== false);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@ import Checkbox from '@/components/ui/Checkbox';
|
|||
|
||||
interface SelectRowProps<TData> {
|
||||
row: Row<TData>;
|
||||
setLastSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
onChangeLastSelected: (newValue: string | undefined) => void;
|
||||
}
|
||||
|
||||
function SelectRow<TData>({ row, setLastSelected }: SelectRowProps<TData>) {
|
||||
function SelectRow<TData>({ row, onChangeLastSelected }: SelectRowProps<TData>) {
|
||||
function handleChange(value: boolean) {
|
||||
setLastSelected(row.id);
|
||||
onChangeLastSelected(row.id);
|
||||
row.toggleSelected(value);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ interface TableBodyProps<TData> {
|
|||
conditionalRowStyles?: IConditionalStyle<TData>[];
|
||||
|
||||
lastSelected: string | undefined;
|
||||
setLastSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
onChangeLastSelected: (newValue: string | undefined) => void;
|
||||
|
||||
onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void;
|
||||
onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void;
|
||||
|
@ -27,7 +27,7 @@ function TableBody<TData>({
|
|||
enableRowSelection,
|
||||
conditionalRowStyles,
|
||||
lastSelected,
|
||||
setLastSelected,
|
||||
onChangeLastSelected,
|
||||
onRowClicked,
|
||||
onRowDoubleClicked
|
||||
}: TableBodyProps<TData>) {
|
||||
|
@ -49,9 +49,9 @@ function TableBody<TData>({
|
|||
newSelection[row.id] = !target.getIsSelected();
|
||||
});
|
||||
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
|
||||
setLastSelected(undefined);
|
||||
onChangeLastSelected(undefined);
|
||||
} else {
|
||||
setLastSelected(target.id);
|
||||
onChangeLastSelected(target.id);
|
||||
target.toggleSelected(!target.getIsSelected());
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ function TableBody<TData>({
|
|||
>
|
||||
{enableRowSelection ? (
|
||||
<td key={`select-${row.id}`} className='pl-3 pr-1 align-middle border-y'>
|
||||
<SelectRow row={row} setLastSelected={setLastSelected} />
|
||||
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
|
||||
</td>
|
||||
) : null}
|
||||
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
|
||||
|
|
|
@ -8,7 +8,7 @@ interface TableHeaderProps<TData> {
|
|||
headPosition?: string;
|
||||
enableRowSelection?: boolean;
|
||||
enableSorting?: boolean;
|
||||
setLastSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
resetLastSelected: () => void;
|
||||
}
|
||||
|
||||
function TableHeader<TData>({
|
||||
|
@ -16,7 +16,7 @@ function TableHeader<TData>({
|
|||
headPosition,
|
||||
enableRowSelection,
|
||||
enableSorting,
|
||||
setLastSelected
|
||||
resetLastSelected
|
||||
}: TableHeaderProps<TData>) {
|
||||
return (
|
||||
<thead
|
||||
|
@ -30,7 +30,7 @@ function TableHeader<TData>({
|
|||
<tr key={headerGroup.id}>
|
||||
{enableRowSelection ? (
|
||||
<th className='pl-3 pr-1 align-middle'>
|
||||
<SelectAll table={table} setLastSelected={setLastSelected} />
|
||||
<SelectAll table={table} resetLastSelected={resetLastSelected} />
|
||||
</th>
|
||||
) : null}
|
||||
{headerGroup.headers.map((header: Header<TData, unknown>) => (
|
||||
|
|
|
@ -7,18 +7,24 @@ import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|||
import AnimateFade from '../wrap/AnimateFade';
|
||||
|
||||
interface LoaderProps {
|
||||
size?: number;
|
||||
/** Scale of the loader from 1 to 10. */
|
||||
scale?: number;
|
||||
|
||||
/** Show a circular loader. */
|
||||
circular?: boolean;
|
||||
}
|
||||
|
||||
function Loader({ size = 10, circular }: LoaderProps) {
|
||||
/**
|
||||
* Displays animated loader.
|
||||
*/
|
||||
function Loader({ scale = 5, circular }: LoaderProps) {
|
||||
const { colors } = useConceptOptions();
|
||||
return (
|
||||
<AnimateFade noFadeIn className='flex justify-center'>
|
||||
{circular ? (
|
||||
<ThreeCircles color={colors.bgPrimary} height={size * 10} width={size * 10} />
|
||||
<ThreeCircles color={colors.bgPrimary} height={scale * 20} width={scale * 20} />
|
||||
) : (
|
||||
<ThreeDots color={colors.bgPrimary} height={size * 10} width={size * 10} radius={size} />
|
||||
<ThreeDots color={colors.bgPrimary} height={scale * 20} width={scale * 20} radius={scale * 2} />
|
||||
)}
|
||||
</AnimateFade>
|
||||
);
|
||||
|
|
|
@ -5,11 +5,19 @@ import { globals } from '@/utils/constants';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface MiniButtonProps extends CProps.Button {
|
||||
/** Icon to display in the button. */
|
||||
icon: React.ReactNode;
|
||||
|
||||
/** Disable hover effect. */
|
||||
noHover?: boolean;
|
||||
|
||||
/** Disable padding. */
|
||||
noPadding?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays small transparent button with an icon.
|
||||
*/
|
||||
function MiniButton({
|
||||
icon,
|
||||
noHover,
|
||||
|
|
|
@ -18,23 +18,46 @@ import MiniButton from './MiniButton';
|
|||
import Overlay from './Overlay';
|
||||
|
||||
export interface ModalProps extends CProps.Styling {
|
||||
/** Title of the modal window. */
|
||||
header?: string;
|
||||
|
||||
/** Text of the submit button. */
|
||||
submitText?: string;
|
||||
|
||||
/** Tooltip for the submit button when the form is invalid. */
|
||||
submitInvalidTooltip?: string;
|
||||
|
||||
/** Indicates that form is readonly. */
|
||||
readonly?: boolean;
|
||||
|
||||
/** Indicates that submit button is enabled. */
|
||||
canSubmit?: boolean;
|
||||
|
||||
/** Indicates that the modal window should be scrollable. */
|
||||
overflowVisible?: boolean;
|
||||
|
||||
/** Callback to be called when the modal window is closed. */
|
||||
hideWindow: () => void;
|
||||
|
||||
/** Callback to be called before submit. */
|
||||
beforeSubmit?: () => boolean;
|
||||
|
||||
/** Callback to be called after submit. */
|
||||
onSubmit?: () => void;
|
||||
|
||||
/** Callback to be called after cancel. */
|
||||
onCancel?: () => void;
|
||||
|
||||
/** Help topic to be displayed in the modal window. */
|
||||
helpTopic?: HelpTopic;
|
||||
|
||||
/** Callback to determine if help should be displayed. */
|
||||
hideHelpWhen?: () => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a customizable modal window.
|
||||
*/
|
||||
function Modal({
|
||||
children,
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@ import clsx from 'clsx';
|
|||
|
||||
import { CProps } from '../props';
|
||||
|
||||
/**
|
||||
* Wraps content in a div with a centered text.
|
||||
*/
|
||||
function NoData({ className, children, ...restProps }: CProps.Div) {
|
||||
return (
|
||||
<div className={clsx('p-3 flex flex-col items-center text-center select-none w-full', className)} {...restProps}>
|
||||
|
|
|
@ -3,11 +3,19 @@ import clsx from 'clsx';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface OverlayProps extends CProps.Styling {
|
||||
/** Id of the overlay. */
|
||||
id?: string;
|
||||
|
||||
/** Classnames for position of the overlay. */
|
||||
position?: string;
|
||||
|
||||
/** Classname for z-index of the overlay. */
|
||||
layer?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a transparent overlay over the main content.
|
||||
*/
|
||||
function Overlay({
|
||||
children,
|
||||
className,
|
||||
|
|
|
@ -5,15 +5,26 @@ import { useMemo } from 'react';
|
|||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import useWindowSize from '@/hooks/useWindowSize';
|
||||
|
||||
/** Maximum width of the viewer. */
|
||||
const MAXIMUM_WIDTH = 1600;
|
||||
|
||||
/** Minimum width of the viewer. */
|
||||
const MINIMUM_WIDTH = 300;
|
||||
|
||||
interface PDFViewerProps {
|
||||
/** PDF file to display. */
|
||||
file?: string;
|
||||
|
||||
/** Offset from the left side of the window. */
|
||||
offsetXpx?: number;
|
||||
|
||||
/** Minimum width of the viewer. */
|
||||
minWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a PDF file using an embedded viewer.
|
||||
*/
|
||||
function PDFViewer({ file, offsetXpx, minWidth = MINIMUM_WIDTH }: PDFViewerProps) {
|
||||
const windowSize = useWindowSize();
|
||||
const { calculateHeight } = useConceptOptions();
|
||||
|
|
|
@ -2,6 +2,9 @@ interface PrettyJsonProps {
|
|||
data: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays JSON data in a formatted string.
|
||||
*/
|
||||
function PrettyJson({ data }: PrettyJsonProps) {
|
||||
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
||||
}
|
||||
|
|
|
@ -6,15 +6,37 @@ import Overlay from './Overlay';
|
|||
import TextInput from './TextInput';
|
||||
|
||||
interface SearchBarProps extends CProps.Styling {
|
||||
value: string;
|
||||
noIcon?: boolean;
|
||||
/** Id of the search bar. */
|
||||
id?: string;
|
||||
|
||||
/** Search query. */
|
||||
query: string;
|
||||
|
||||
/** Placeholder text. */
|
||||
placeholder?: string;
|
||||
onChange?: (newValue: string) => void;
|
||||
|
||||
/** Callback to be called when the search query changes. */
|
||||
onChangeQuery?: (newValue: string) => void;
|
||||
|
||||
/** Disable search icon. */
|
||||
noIcon?: boolean;
|
||||
|
||||
/** Disable border. */
|
||||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
function SearchBar({ id, value, noIcon, onChange, noBorder, placeholder = 'Поиск', ...restProps }: SearchBarProps) {
|
||||
/**
|
||||
* Displays a search bar with a search icon and text input.
|
||||
*/
|
||||
function SearchBar({
|
||||
id,
|
||||
query,
|
||||
noIcon,
|
||||
onChangeQuery,
|
||||
noBorder,
|
||||
placeholder = 'Поиск',
|
||||
...restProps
|
||||
}: SearchBarProps) {
|
||||
return (
|
||||
<div {...restProps}>
|
||||
{!noIcon ? (
|
||||
|
@ -29,8 +51,8 @@ function SearchBar({ id, value, noIcon, onChange, noBorder, placeholder = 'По
|
|||
type='search'
|
||||
className={clsx('outline-none bg-transparent', !noIcon && 'pl-10')}
|
||||
noBorder={noBorder}
|
||||
value={value}
|
||||
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
||||
value={query}
|
||||
onChange={event => (onChangeQuery ? onChangeQuery(event.target.value) : undefined)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -45,6 +45,9 @@ export interface SelectMultiProps<Option, Group extends GroupBase<Option> = Grou
|
|||
noPortal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a multi-select component.
|
||||
*/
|
||||
function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
|
||||
noPortal,
|
||||
...restProps
|
||||
|
|
|
@ -46,6 +46,9 @@ export interface SelectSingleProps<Option, Group extends GroupBase<Option> = Gro
|
|||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a single-select component.
|
||||
*/
|
||||
function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
|
||||
noPortal,
|
||||
noBorder,
|
||||
|
|
|
@ -11,22 +11,38 @@ import MiniButton from './MiniButton';
|
|||
import Overlay from './Overlay';
|
||||
|
||||
interface SelectTreeProps<ItemType> extends CProps.Styling {
|
||||
items: ItemType[];
|
||||
/** Current value. */
|
||||
value: ItemType;
|
||||
setValue: (newItem: ItemType) => void;
|
||||
getParent: (item: ItemType) => ItemType;
|
||||
getLabel: (item: ItemType) => string;
|
||||
getDescription: (item: ItemType) => string;
|
||||
|
||||
/** List of available items. */
|
||||
items: ItemType[];
|
||||
|
||||
/** Prefix for the ids of the elements. */
|
||||
prefix: string;
|
||||
|
||||
/** Callback to be called when the value changes. */
|
||||
onChangeValue: (newItem: ItemType) => void;
|
||||
|
||||
/** Callback providing the parent of the item. */
|
||||
getParent: (item: ItemType) => ItemType;
|
||||
|
||||
/** Callback providing the label of the item. */
|
||||
getLabel: (item: ItemType) => string;
|
||||
|
||||
/** Callback providing the description of the item. */
|
||||
getDescription: (item: ItemType) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a tree of items and allows user to select one.
|
||||
*/
|
||||
function SelectTree<ItemType>({
|
||||
items,
|
||||
value,
|
||||
getParent,
|
||||
getLabel,
|
||||
getDescription,
|
||||
setValue,
|
||||
onChangeValue,
|
||||
prefix,
|
||||
...restProps
|
||||
}: SelectTreeProps<ItemType>) {
|
||||
|
@ -71,9 +87,9 @@ function SelectTree<ItemType>({
|
|||
(event: CProps.EventMouse, target: ItemType) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setValue(target);
|
||||
onChangeValue(target);
|
||||
},
|
||||
[setValue]
|
||||
[onChangeValue]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,13 +5,22 @@ import { globals } from '@/utils/constants';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface SelectorButtonProps extends CProps.Button {
|
||||
/** Text to display in the button. */
|
||||
text?: string;
|
||||
|
||||
/** Icon to display in the button. */
|
||||
icon?: React.ReactNode;
|
||||
|
||||
/** Classnames for the colors of the button. */
|
||||
colors?: string;
|
||||
|
||||
/** Indicates if button background should be transparent. */
|
||||
transparent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a button with an icon and text that opens a dropdown menu.
|
||||
*/
|
||||
function SelectorButton({
|
||||
text,
|
||||
icon,
|
||||
|
|
|
@ -3,11 +3,19 @@ import clsx from 'clsx';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface SubmitButtonProps extends CProps.Button {
|
||||
/** Text to display in the button. */
|
||||
text?: string;
|
||||
loading?: boolean;
|
||||
|
||||
/** Icon to display in the button. */
|
||||
icon?: React.ReactNode;
|
||||
|
||||
/** Indicates that loading is in progress. */
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays submit type button with icon and text.
|
||||
*/
|
||||
function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...restProps }: SubmitButtonProps) {
|
||||
return (
|
||||
<button
|
||||
|
|
|
@ -7,9 +7,13 @@ import { globals } from '@/utils/constants';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface TabLabelProps extends Omit<TabPropsImpl, 'children'>, CProps.Titled {
|
||||
/** Label to display in the tab. */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a tab header with a label.
|
||||
*/
|
||||
function TabLabel({ label, title, titleHtml, hideTitle, className, ...otherProps }: TabLabelProps) {
|
||||
return (
|
||||
<TabImpl
|
||||
|
|
|
@ -4,11 +4,19 @@ import { CProps } from '../props';
|
|||
import Label from './Label';
|
||||
|
||||
export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea {
|
||||
/** Indicates that padding should be minimal. */
|
||||
dense?: boolean;
|
||||
|
||||
/** Disable resize when content overflows. */
|
||||
noResize?: boolean;
|
||||
|
||||
/** Disable resize to fit content. */
|
||||
fitContent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a customizable textarea with a label.
|
||||
*/
|
||||
function TextArea({
|
||||
id,
|
||||
label,
|
||||
|
|
|
@ -6,11 +6,19 @@ import { truncateToLastWord } from '@/utils/utils';
|
|||
import { CProps } from '../props';
|
||||
|
||||
export interface TextContentProps extends CProps.Styling {
|
||||
/** Text to display. */
|
||||
text: string;
|
||||
|
||||
/** Maximum number of symbols to display. */
|
||||
maxLength?: number;
|
||||
|
||||
/** Disable full text in a tooltip. */
|
||||
noTooltip?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays text limited to a certain number of symbols.
|
||||
*/
|
||||
function TextContent({ className, text, maxLength, noTooltip, ...restProps }: TextContentProps) {
|
||||
const truncated = maxLength ? truncateToLastWord(text, maxLength) : text;
|
||||
const isTruncated = maxLength && text.length > maxLength;
|
||||
|
|
|
@ -4,7 +4,10 @@ import { CProps } from '../props';
|
|||
import Label from './Label';
|
||||
|
||||
interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
|
||||
/** Indicates that padding should be minimal. */
|
||||
dense?: boolean;
|
||||
|
||||
/** Capture enter key. */
|
||||
allowEnter?: boolean;
|
||||
}
|
||||
|
||||
|
@ -14,6 +17,9 @@ function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a customizable input with a label.
|
||||
*/
|
||||
function TextInput({
|
||||
id,
|
||||
label,
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface TextURLProps {
|
||||
/** Text to display. */
|
||||
text: string;
|
||||
|
||||
/** Tooltip for the link. */
|
||||
title?: string;
|
||||
|
||||
/** URL to link to. */
|
||||
href?: string;
|
||||
|
||||
/** Color of the link. */
|
||||
color?: string;
|
||||
|
||||
/** Callback to be called when the link is clicked. */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a text with a clickable link.
|
||||
*/
|
||||
function TextURL({ text, href, title, color = 'clr-text-url', onClick }: TextURLProps) {
|
||||
const design = `cursor-pointer hover:underline ${color}`;
|
||||
if (href) {
|
||||
|
|
|
@ -10,10 +10,16 @@ import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
|||
export type { PlacesType } from 'react-tooltip';
|
||||
|
||||
interface TooltipProps extends Omit<ITooltip, 'variant'> {
|
||||
layer?: string;
|
||||
/** Text to display in the tooltip. */
|
||||
text?: string;
|
||||
|
||||
/** Classname for z-index */
|
||||
layer?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays content in a tooltip container.
|
||||
*/
|
||||
function Tooltip({
|
||||
text,
|
||||
children,
|
||||
|
|
|
@ -7,16 +7,34 @@ import { CProps } from '../props';
|
|||
import MiniButton from './MiniButton';
|
||||
|
||||
interface ValueIconProps extends CProps.Styling, CProps.Titled {
|
||||
/** Id of the component. */
|
||||
id?: string;
|
||||
icon: React.ReactNode;
|
||||
|
||||
/** Value to display. */
|
||||
value: string | number;
|
||||
|
||||
/** Icon to display. */
|
||||
icon: React.ReactNode;
|
||||
|
||||
/** Classname for the text. */
|
||||
textClassName?: string;
|
||||
|
||||
/** Callback to be called when the component is clicked. */
|
||||
onClick?: (event: CProps.EventMouse) => void;
|
||||
|
||||
/** Number of symbols to display in a small size. */
|
||||
smallThreshold?: number;
|
||||
|
||||
/** Indicates that padding should be minimal. */
|
||||
dense?: boolean;
|
||||
|
||||
/** Disable interaction. */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a value with an icon that can be clicked.
|
||||
*/
|
||||
function ValueIcon({
|
||||
id,
|
||||
dense,
|
||||
|
|
|
@ -3,12 +3,22 @@ import clsx from 'clsx';
|
|||
import { CProps } from '../props';
|
||||
|
||||
interface ValueLabeledProps extends CProps.Styling {
|
||||
/** Id of the component. */
|
||||
id?: string;
|
||||
|
||||
/** Label to display. */
|
||||
label: string;
|
||||
|
||||
/** Value to display. */
|
||||
text: string | number;
|
||||
|
||||
/** Tooltip for the component. */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a labeled value.
|
||||
*/
|
||||
function ValueLabeled({ id, label, text, title, className, ...restProps }: ValueLabeledProps) {
|
||||
return (
|
||||
<div className={clsx('flex justify-between gap-6', className)} {...restProps}>
|
||||
|
|
|
@ -4,11 +4,19 @@ import { CProps } from '../props';
|
|||
import ValueIcon from './ValueIcon';
|
||||
|
||||
interface ValueStatsProps extends CProps.Styling, CProps.Titled {
|
||||
/** Id of the component. */
|
||||
id: string;
|
||||
|
||||
/** Icon to display. */
|
||||
icon: React.ReactNode;
|
||||
|
||||
/** Value to display. */
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays statistics value with an icon.
|
||||
*/
|
||||
function ValueStats(props: ValueStatsProps) {
|
||||
return <ValueIcon dense smallThreshold={PARAMETER.statSmallThreshold} textClassName='min-w-[1.4rem]' {...props} />;
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ export const OptionsState = ({ children }: React.PropsWithChildren) => {
|
|||
id={`${globals.tooltip}`}
|
||||
layer='z-topmost'
|
||||
place='right-start'
|
||||
className='mt-8 max-w-[20rem]'
|
||||
className='mt-8 max-w-[20rem] break-words'
|
||||
/>
|
||||
<Tooltip
|
||||
float
|
||||
|
|
|
@ -103,15 +103,15 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
|
|||
<TabInputOperation
|
||||
oss={oss}
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
onChangeAlias={setAlias}
|
||||
comment={comment}
|
||||
setComment={setComment}
|
||||
onChangeComment={setComment}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
onChangeTitle={setTitle}
|
||||
attachedID={attachedID}
|
||||
setAttachedID={setAttachedID}
|
||||
onChangeAttachedID={setAttachedID}
|
||||
createSchema={createSchema}
|
||||
setCreateSchema={setCreateSchema}
|
||||
onChangeCreateSchema={setCreateSchema}
|
||||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
|
@ -124,11 +124,11 @@ function DlgCreateOperation({ hideWindow, oss, onCreate, initialInputs }: DlgCre
|
|||
<TabSynthesisOperation
|
||||
oss={oss}
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
onChangeAlias={setAlias}
|
||||
comment={comment}
|
||||
setComment={setComment}
|
||||
onChangeComment={setComment}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
onChangeTitle={setTitle}
|
||||
inputs={inputs}
|
||||
setInputs={setInputs}
|
||||
/>
|
||||
|
|
|
@ -18,29 +18,29 @@ import { sortItemsForOSS } from '@/models/ossAPI';
|
|||
interface TabInputOperationProps {
|
||||
oss: IOperationSchema;
|
||||
alias: string;
|
||||
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeAlias: (newValue: string) => void;
|
||||
title: string;
|
||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeTitle: (newValue: string) => void;
|
||||
comment: string;
|
||||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeComment: (newValue: string) => void;
|
||||
attachedID: LibraryItemID | undefined;
|
||||
setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>;
|
||||
onChangeAttachedID: (newValue: LibraryItemID | undefined) => void;
|
||||
createSchema: boolean;
|
||||
setCreateSchema: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onChangeCreateSchema: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function TabInputOperation({
|
||||
oss,
|
||||
alias,
|
||||
setAlias,
|
||||
onChangeAlias,
|
||||
title,
|
||||
setTitle,
|
||||
onChangeTitle,
|
||||
comment,
|
||||
setComment,
|
||||
onChangeComment,
|
||||
attachedID,
|
||||
setAttachedID,
|
||||
onChangeAttachedID,
|
||||
createSchema,
|
||||
setCreateSchema
|
||||
onChangeCreateSchema
|
||||
}: TabInputOperationProps) {
|
||||
const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]);
|
||||
const library = useLibrary();
|
||||
|
@ -48,9 +48,9 @@ function TabInputOperation({
|
|||
|
||||
useEffect(() => {
|
||||
if (createSchema) {
|
||||
setAttachedID(undefined);
|
||||
onChangeAttachedID(undefined);
|
||||
}
|
||||
}, [createSchema, setAttachedID]);
|
||||
}, [createSchema, onChangeAttachedID]);
|
||||
|
||||
return (
|
||||
<AnimateFade className='cc-column'>
|
||||
|
@ -58,7 +58,7 @@ function TabInputOperation({
|
|||
id='operation_title'
|
||||
label='Полное название'
|
||||
value={title}
|
||||
onChange={event => setTitle(event.target.value)}
|
||||
onChange={event => onChangeTitle(event.target.value)}
|
||||
disabled={attachedID !== undefined}
|
||||
/>
|
||||
<div className='flex gap-6'>
|
||||
|
@ -67,7 +67,7 @@ function TabInputOperation({
|
|||
label='Сокращение'
|
||||
className='w-[16rem]'
|
||||
value={alias}
|
||||
onChange={event => setAlias(event.target.value)}
|
||||
onChange={event => onChangeAlias(event.target.value)}
|
||||
disabled={attachedID !== undefined}
|
||||
/>
|
||||
|
||||
|
@ -77,7 +77,7 @@ function TabInputOperation({
|
|||
noResize
|
||||
rows={3}
|
||||
value={comment}
|
||||
onChange={event => setComment(event.target.value)}
|
||||
onChange={event => onChangeComment(event.target.value)}
|
||||
disabled={attachedID !== undefined}
|
||||
/>
|
||||
</div>
|
||||
|
@ -90,13 +90,13 @@ function TabInputOperation({
|
|||
noHover
|
||||
noPadding
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setAttachedID(undefined)}
|
||||
onClick={() => onChangeAttachedID(undefined)}
|
||||
disabled={attachedID == undefined}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
value={createSchema}
|
||||
setValue={setCreateSchema}
|
||||
setValue={onChangeCreateSchema}
|
||||
label='Создать новую схему'
|
||||
titleHtml='Создать пустую схему для загрузки'
|
||||
/>
|
||||
|
@ -106,7 +106,7 @@ function TabInputOperation({
|
|||
items={sortedItems}
|
||||
value={attachedID}
|
||||
itemType={LibraryItemType.RSFORM}
|
||||
onSelectValue={setAttachedID}
|
||||
onSelectValue={onChangeAttachedID}
|
||||
rows={8}
|
||||
baseFilter={baseFilter}
|
||||
/>
|
||||
|
|
|
@ -10,11 +10,11 @@ import PickMultiOperation from '../../components/select/PickMultiOperation';
|
|||
interface TabSynthesisOperationProps {
|
||||
oss: IOperationSchema;
|
||||
alias: string;
|
||||
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeAlias: (newValue: string) => void;
|
||||
title: string;
|
||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeTitle: (newValue: string) => void;
|
||||
comment: string;
|
||||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeComment: (newValue: string) => void;
|
||||
inputs: OperationID[];
|
||||
setInputs: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||
}
|
||||
|
@ -22,11 +22,11 @@ interface TabSynthesisOperationProps {
|
|||
function TabSynthesisOperation({
|
||||
oss,
|
||||
alias,
|
||||
setAlias,
|
||||
onChangeAlias,
|
||||
title,
|
||||
setTitle,
|
||||
onChangeTitle,
|
||||
comment,
|
||||
setComment,
|
||||
onChangeComment,
|
||||
inputs,
|
||||
setInputs
|
||||
}: TabSynthesisOperationProps) {
|
||||
|
@ -36,7 +36,7 @@ function TabSynthesisOperation({
|
|||
id='operation_title'
|
||||
label='Полное название'
|
||||
value={title}
|
||||
onChange={event => setTitle(event.target.value)}
|
||||
onChange={event => onChangeTitle(event.target.value)}
|
||||
/>
|
||||
<div className='flex gap-6'>
|
||||
<TextInput
|
||||
|
@ -44,7 +44,7 @@ function TabSynthesisOperation({
|
|||
label='Сокращение'
|
||||
className='w-[16rem]'
|
||||
value={alias}
|
||||
onChange={event => setAlias(event.target.value)}
|
||||
onChange={event => onChangeAlias(event.target.value)}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
|
@ -53,7 +53,7 @@ function TabSynthesisOperation({
|
|||
noResize
|
||||
rows={3}
|
||||
value={comment}
|
||||
onChange={event => setComment(event.target.value)}
|
||||
onChange={event => onChangeComment(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -141,11 +141,11 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
|
|||
<TabPanel>
|
||||
<TabOperation
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
onChangeAlias={setAlias}
|
||||
comment={comment}
|
||||
setComment={setComment}
|
||||
onChangeComment={setComment}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
onChangeTitle={setTitle}
|
||||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
|
|
|
@ -4,21 +4,21 @@ import AnimateFade from '@/components/wrap/AnimateFade';
|
|||
|
||||
interface TabOperationProps {
|
||||
alias: string;
|
||||
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeAlias: (newValue: string) => void;
|
||||
title: string;
|
||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeTitle: (newValue: string) => void;
|
||||
comment: string;
|
||||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeComment: (newValue: string) => void;
|
||||
}
|
||||
|
||||
function TabOperation({ alias, setAlias, title, setTitle, comment, setComment }: TabOperationProps) {
|
||||
function TabOperation({ alias, onChangeAlias, title, onChangeTitle, comment, onChangeComment }: TabOperationProps) {
|
||||
return (
|
||||
<AnimateFade className='cc-column'>
|
||||
<TextInput
|
||||
id='operation_title'
|
||||
label='Полное название'
|
||||
value={title}
|
||||
onChange={event => setTitle(event.target.value)}
|
||||
onChange={event => onChangeTitle(event.target.value)}
|
||||
/>
|
||||
<div className='flex gap-6'>
|
||||
<TextInput
|
||||
|
@ -26,7 +26,7 @@ function TabOperation({ alias, setAlias, title, setTitle, comment, setComment }:
|
|||
label='Сокращение'
|
||||
className='w-[16rem]'
|
||||
value={alias}
|
||||
onChange={event => setAlias(event.target.value)}
|
||||
onChange={event => onChangeAlias(event.target.value)}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
|
@ -35,7 +35,7 @@ function TabOperation({ alias, setAlias, title, setTitle, comment, setComment }:
|
|||
noResize
|
||||
rows={3}
|
||||
value={comment}
|
||||
onChange={event => setComment(event.target.value)}
|
||||
onChange={event => onChangeComment(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</AnimateFade>
|
||||
|
|
|
@ -45,7 +45,12 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
|
|||
const entityPanel = useMemo(
|
||||
() => (
|
||||
<TabPanel>
|
||||
<TabEntityReference initial={initial} schema={schema} setReference={setReference} setIsValid={setIsValid} />
|
||||
<TabEntityReference
|
||||
initial={initial}
|
||||
schema={schema}
|
||||
onChangeReference={setReference}
|
||||
onChangeValid={setIsValid}
|
||||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
[initial, schema]
|
||||
|
@ -54,7 +59,7 @@ function DlgEditReference({ hideWindow, schema, initial, onSave }: DlgEditRefere
|
|||
const syntacticPanel = useMemo(
|
||||
() => (
|
||||
<TabPanel>
|
||||
<TabSyntacticReference initial={initial} setReference={setReference} setIsValid={setIsValid} />
|
||||
<TabSyntacticReference initial={initial} onChangeReference={setReference} onChangeValid={setIsValid} />
|
||||
</TabPanel>
|
||||
),
|
||||
[initial]
|
||||
|
|
|
@ -21,11 +21,11 @@ import { IReferenceInputState } from './DlgEditReference';
|
|||
interface TabEntityReferenceProps {
|
||||
initial: IReferenceInputState;
|
||||
schema: IRSForm;
|
||||
setIsValid: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setReference: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeValid: (newValue: boolean) => void;
|
||||
onChangeReference: (newValue: string) => void;
|
||||
}
|
||||
|
||||
function TabEntityReference({ initial, schema, setIsValid, setReference }: TabEntityReferenceProps) {
|
||||
function TabEntityReference({ initial, schema, onChangeValid, onChangeReference }: TabEntityReferenceProps) {
|
||||
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined);
|
||||
const [alias, setAlias] = useState('');
|
||||
const [term, setTerm] = useState('');
|
||||
|
@ -43,9 +43,9 @@ function TabEntityReference({ initial, schema, setIsValid, setReference }: TabEn
|
|||
|
||||
// Produce result
|
||||
useEffect(() => {
|
||||
setIsValid(alias !== '' && selectedGrams.length > 0);
|
||||
setReference(`@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`);
|
||||
}, [alias, selectedGrams, setIsValid, setReference]);
|
||||
onChangeValid(alias !== '' && selectedGrams.length > 0);
|
||||
onChangeReference(`@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`);
|
||||
}, [alias, selectedGrams, onChangeValid, onChangeReference]);
|
||||
|
||||
// Update term when alias changes
|
||||
useEffect(() => {
|
||||
|
@ -105,7 +105,7 @@ function TabEntityReference({ initial, schema, setIsValid, setReference }: TabEn
|
|||
className='flex-grow'
|
||||
menuPlacement='top'
|
||||
value={selectedGrams}
|
||||
setValue={setSelectedGrams}
|
||||
onChangeValue={setSelectedGrams}
|
||||
/>
|
||||
</div>
|
||||
</AnimateFade>
|
||||
|
|
|
@ -11,11 +11,11 @@ import { IReferenceInputState } from './DlgEditReference';
|
|||
|
||||
interface TabSyntacticReferenceProps {
|
||||
initial: IReferenceInputState;
|
||||
setIsValid: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setReference: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeValid: (newValue: boolean) => void;
|
||||
onChangeReference: (newValue: string) => void;
|
||||
}
|
||||
|
||||
function TabSyntacticReference({ initial, setIsValid, setReference }: TabSyntacticReferenceProps) {
|
||||
function TabSyntacticReference({ initial, onChangeValid, onChangeReference }: TabSyntacticReferenceProps) {
|
||||
const [nominal, setNominal] = useState('');
|
||||
const [offset, setOffset] = useState(1);
|
||||
|
||||
|
@ -39,9 +39,9 @@ function TabSyntacticReference({ initial, setIsValid, setReference }: TabSyntact
|
|||
}, [initial]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsValid(nominal !== '' && offset !== 0);
|
||||
setReference(`@{${offset}|${nominal}}`);
|
||||
}, [nominal, offset, setIsValid, setReference]);
|
||||
onChangeValid(nominal !== '' && offset !== 0);
|
||||
onChangeReference(`@{${offset}|${nominal}}`);
|
||||
}, [nominal, offset, onChangeValid, onChangeReference]);
|
||||
|
||||
return (
|
||||
<AnimateFade className='flex flex-col gap-2'>
|
||||
|
|
|
@ -170,7 +170,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
|||
placeholder='Выберите граммемы'
|
||||
className='min-w-[15rem] h-fit'
|
||||
value={inputGrams}
|
||||
setValue={setInputGrams}
|
||||
onChangeValue={setInputGrams}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
74
rsconcept/frontend/src/dialogs/DlgShowAST/ASTFlow.tsx
Normal file
74
rsconcept/frontend/src/dialogs/DlgShowAST/ASTFlow.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
'use client';
|
||||
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { Edge, MarkerType, Node, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow';
|
||||
|
||||
import { SyntaxTree } from '@/models/rslang';
|
||||
|
||||
import { ASTEdgeTypes } from './graph/ASTEdgeTypes';
|
||||
import { applyLayout } from './graph/ASTLayout';
|
||||
import { ASTNodeTypes } from './graph/ASTNodeTypes';
|
||||
|
||||
interface ASTFlowProps {
|
||||
data: SyntaxTree;
|
||||
onNodeEnter: (node: Node) => void;
|
||||
onNodeLeave: (node: Node) => void;
|
||||
}
|
||||
|
||||
function ASTFlow({ data, onNodeEnter, onNodeLeave }: ASTFlowProps) {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges] = useEdgesState([]);
|
||||
const flow = useReactFlow();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const newNodes = data.map(node => ({
|
||||
id: String(node.uid),
|
||||
data: node,
|
||||
position: { x: 0, y: 0 },
|
||||
type: 'token'
|
||||
}));
|
||||
|
||||
const newEdges: Edge[] = [];
|
||||
data.forEach(node => {
|
||||
if (node.parent !== node.uid) {
|
||||
newEdges.push({
|
||||
id: String(node.uid),
|
||||
source: String(node.parent),
|
||||
target: String(node.uid),
|
||||
type: 'dynamic',
|
||||
focusable: false,
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
width: 20,
|
||||
height: 20
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
applyLayout(newNodes, newEdges);
|
||||
|
||||
setNodes(newNodes);
|
||||
setEdges(newEdges);
|
||||
}, [data, setNodes, setEdges, flow]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
edgesFocusable={false}
|
||||
nodesFocusable={false}
|
||||
onNodeMouseEnter={(_, node) => onNodeEnter(node)}
|
||||
onNodeMouseLeave={(_, node) => onNodeLeave(node)}
|
||||
onNodesChange={onNodesChange}
|
||||
nodeTypes={ASTNodeTypes}
|
||||
edgeTypes={ASTEdgeTypes}
|
||||
fitView
|
||||
maxZoom={2}
|
||||
minZoom={0.5}
|
||||
nodesConnectable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ASTFlow;
|
|
@ -1,17 +1,16 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
import { Node } from 'reactflow';
|
||||
|
||||
import GraphUI, { GraphEdge, GraphNode } from '@/components/ui/GraphUI';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { SyntaxTree } from '@/models/rslang';
|
||||
import { graphDarkT, graphLightT } from '@/styling/color';
|
||||
import { colorBgSyntaxTree } from '@/styling/color';
|
||||
import { resources } from '@/utils/constants';
|
||||
import { labelSyntaxTree } from '@/utils/labels';
|
||||
|
||||
import ASTFlow from './ASTFlow';
|
||||
|
||||
interface DlgShowASTProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
syntaxTree: SyntaxTree;
|
||||
|
@ -19,36 +18,11 @@ interface DlgShowASTProps extends Pick<ModalProps, 'hideWindow'> {
|
|||
}
|
||||
|
||||
function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
||||
const { darkMode, colors } = useConceptOptions();
|
||||
const { colors } = useConceptOptions();
|
||||
const [hoverID, setHoverID] = useState<number | undefined>(undefined);
|
||||
const hoverNode = useMemo(() => syntaxTree.find(node => node.uid === hoverID), [hoverID, syntaxTree]);
|
||||
|
||||
const nodes: GraphNode[] = useMemo(
|
||||
() =>
|
||||
syntaxTree.map(node => ({
|
||||
id: String(syntaxTree.length - node.uid), // invert order of IDs to force correct ordering in graph layout
|
||||
label: labelSyntaxTree(node),
|
||||
fill: colorBgSyntaxTree(node, colors)
|
||||
})),
|
||||
[syntaxTree, colors]
|
||||
);
|
||||
|
||||
const edges: GraphEdge[] = useMemo(() => {
|
||||
const result: GraphEdge[] = [];
|
||||
syntaxTree.forEach(node => {
|
||||
if (node.parent !== node.uid) {
|
||||
result.push({
|
||||
id: String(node.uid),
|
||||
source: String(syntaxTree.length - node.parent),
|
||||
target: String(syntaxTree.length - node.uid)
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}, [syntaxTree]);
|
||||
|
||||
const handleHoverIn = useCallback((node: GraphNode) => setHoverID(syntaxTree.length - Number(node.id)), [syntaxTree]);
|
||||
|
||||
const handleHoverIn = useCallback((node: Node) => setHoverID(Number(node.id)), []);
|
||||
const handleHoverOut = useCallback(() => setHoverID(undefined), []);
|
||||
|
||||
return (
|
||||
|
@ -72,18 +46,9 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
|||
</div>
|
||||
) : null}
|
||||
</Overlay>
|
||||
<div className='flex-grow relative'>
|
||||
<GraphUI
|
||||
animated={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
layoutType='hierarchicalTd'
|
||||
labelFontUrl={resources.graph_font}
|
||||
theme={darkMode ? graphDarkT : graphLightT}
|
||||
onNodePointerOver={handleHoverIn}
|
||||
onNodePointerOut={handleHoverOut}
|
||||
/>
|
||||
</div>
|
||||
<ReactFlowProvider>
|
||||
<ASTFlow data={syntaxTree} onNodeEnter={handleHoverIn} onNodeLeave={handleHoverOut} />
|
||||
</ReactFlowProvider>
|
||||
</Modal>
|
||||
);
|
||||
}
|
28
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdge.tsx
Normal file
28
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTEdge.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { EdgeProps, getStraightPath } from 'reactflow';
|
||||
|
||||
const NODE_RADIUS = 20;
|
||||
const EDGE_RADIUS = 25;
|
||||
|
||||
function ASTEdge({ id, markerEnd, style, ...props }: EdgeProps) {
|
||||
const scale =
|
||||
EDGE_RADIUS /
|
||||
Math.sqrt(
|
||||
Math.pow(props.sourceX - props.targetX, 2) +
|
||||
Math.pow(Math.abs(props.sourceY - props.targetY) + 2 * NODE_RADIUS, 2)
|
||||
);
|
||||
|
||||
const [path] = getStraightPath({
|
||||
sourceX: props.sourceX - (props.sourceX - props.targetX) * scale,
|
||||
sourceY: props.sourceY - (props.sourceY - props.targetY - 2 * NODE_RADIUS) * scale - NODE_RADIUS,
|
||||
targetX: props.targetX + (props.sourceX - props.targetX) * scale,
|
||||
targetY: props.targetY + (props.sourceY - props.targetY - 2 * NODE_RADIUS) * scale + NODE_RADIUS
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ASTEdge;
|
|
@ -0,0 +1,7 @@
|
|||
import { EdgeTypes } from 'reactflow';
|
||||
|
||||
import ASTEdge from './ASTEdge';
|
||||
|
||||
export const ASTEdgeTypes: EdgeTypes = {
|
||||
dynamic: ASTEdge
|
||||
};
|
35
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTLayout.ts
Normal file
35
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTLayout.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import dagre from '@dagrejs/dagre';
|
||||
import { Edge, Node } from 'reactflow';
|
||||
|
||||
import { ISyntaxTreeNode } from '@/models/rslang';
|
||||
|
||||
const NODE_WIDTH = 44;
|
||||
const NODE_HEIGHT = 44;
|
||||
const HOR_SEPARATION = 40;
|
||||
const VERT_SEPARATION = 40;
|
||||
|
||||
export function applyLayout(nodes: Node<ISyntaxTreeNode>[], edges: Edge[]) {
|
||||
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
dagreGraph.setGraph({
|
||||
rankdir: 'TB',
|
||||
ranksep: VERT_SEPARATION,
|
||||
nodesep: HOR_SEPARATION,
|
||||
ranker: 'network-simplex',
|
||||
align: undefined
|
||||
});
|
||||
nodes.forEach(node => {
|
||||
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||
});
|
||||
|
||||
edges.forEach(edge => {
|
||||
dagreGraph.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(dagreGraph);
|
||||
|
||||
nodes.forEach(node => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id);
|
||||
node.position.x = nodeWithPosition.x - NODE_WIDTH / 2;
|
||||
node.position.y = nodeWithPosition.y - NODE_HEIGHT / 2;
|
||||
});
|
||||
}
|
44
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNode.tsx
Normal file
44
rsconcept/frontend/src/dialogs/DlgShowAST/graph/ASTNode.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { ISyntaxTreeNode } from '@/models/rslang';
|
||||
import { colorBgSyntaxTree } from '@/styling/color';
|
||||
import { labelSyntaxTree } from '@/utils/labels';
|
||||
|
||||
/**
|
||||
* Represents graph AST node internal data.
|
||||
*/
|
||||
interface ASTNodeInternal {
|
||||
id: string;
|
||||
data: ISyntaxTreeNode;
|
||||
dragging: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}
|
||||
|
||||
function ASTNode(node: ASTNodeInternal) {
|
||||
const { colors } = useConceptOptions();
|
||||
const label = useMemo(() => labelSyntaxTree(node.data), [node.data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type='target' position={Position.Top} style={{ opacity: 0 }} />
|
||||
<div
|
||||
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
|
||||
style={{ backgroundColor: colorBgSyntaxTree(node.data, colors) }}
|
||||
/>
|
||||
<Handle type='source' position={Position.Bottom} style={{ opacity: 0 }} />
|
||||
<div
|
||||
className='font-math mt-1 w-fit px-1 text-center translate-x-[calc(-50%+20px)]'
|
||||
style={{ backgroundColor: colors.bgDefault, fontSize: label.length > 3 ? 12 : 14 }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ASTNode;
|
|
@ -0,0 +1,7 @@
|
|||
import { NodeTypes } from 'reactflow';
|
||||
|
||||
import ASTNode from './ASTNode';
|
||||
|
||||
export const ASTNodeTypes: NodeTypes = {
|
||||
token: ASTNode
|
||||
};
|
1
rsconcept/frontend/src/dialogs/DlgShowAST/index.tsx
Normal file
1
rsconcept/frontend/src/dialogs/DlgShowAST/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './DlgShowAST';
|
|
@ -4,7 +4,6 @@ import { useLayoutEffect } from 'react';
|
|||
import { Edge, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow';
|
||||
|
||||
import { TMGraph } from '@/models/TMGraph';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { TMGraphEdgeTypes } from './graph/MGraphEdgeTypes';
|
||||
import { applyLayout } from './graph/MGraphLayout';
|
||||
|
@ -15,7 +14,7 @@ interface MGraphFlowProps {
|
|||
}
|
||||
|
||||
function MGraphFlow({ data }: MGraphFlowProps) {
|
||||
const [nodes, setNodes] = useNodesState([]);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges] = useEdgesState([]);
|
||||
const flow = useReactFlow();
|
||||
|
||||
|
@ -49,11 +48,10 @@ function MGraphFlow({ data }: MGraphFlowProps) {
|
|||
});
|
||||
});
|
||||
|
||||
applyLayout(newNodes);
|
||||
applyLayout(newNodes, newEdges);
|
||||
|
||||
setNodes(newNodes);
|
||||
setEdges(newEdges);
|
||||
flow.fitView({ duration: PARAMETER.zoomDuration });
|
||||
}, [data, setNodes, setEdges, flow]);
|
||||
|
||||
return (
|
||||
|
@ -62,14 +60,13 @@ function MGraphFlow({ data }: MGraphFlowProps) {
|
|||
edges={edges}
|
||||
edgesFocusable={false}
|
||||
nodesFocusable={false}
|
||||
onNodesChange={onNodesChange}
|
||||
nodeTypes={TMGraphNodeTypes}
|
||||
edgeTypes={TMGraphEdgeTypes}
|
||||
fitView
|
||||
maxZoom={2}
|
||||
minZoom={0.5}
|
||||
nodesConnectable={false}
|
||||
snapToGrid={true}
|
||||
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,171 +1,38 @@
|
|||
import { Node } from 'reactflow';
|
||||
import dagre from '@dagrejs/dagre';
|
||||
import { Edge, Node } from 'reactflow';
|
||||
|
||||
import { Graph } from '@/models/Graph';
|
||||
import { TMGraphNode } from '@/models/TMGraph';
|
||||
|
||||
export function applyLayout(nodes: Node<TMGraphNode>[]) {
|
||||
new LayoutManager(nodes).execute();
|
||||
}
|
||||
const NODE_WIDTH = 44;
|
||||
const NODE_HEIGHT = 44;
|
||||
const HOR_SEPARATION = 40;
|
||||
const VERT_SEPARATION = 40;
|
||||
|
||||
const UNIT_HEIGHT = 100;
|
||||
const UNIT_WIDTH = 100;
|
||||
const MIN_NODE_DISTANCE = 80;
|
||||
const LAYOUT_ITERATIONS = 8;
|
||||
const BOOLEAN_WEIGHT = 2;
|
||||
const CARTESIAN_WEIGHT = 1;
|
||||
|
||||
class LayoutManager {
|
||||
nodes: Node<TMGraphNode>[];
|
||||
|
||||
graph = new Graph();
|
||||
ranks = new Map<number, number>();
|
||||
posX = new Map<number, number>();
|
||||
posY = new Map<number, number>();
|
||||
|
||||
maxRank = 0;
|
||||
virtualCount = 0;
|
||||
layers: number[][] = [];
|
||||
|
||||
constructor(nodes: Node<TMGraphNode>[]) {
|
||||
this.nodes = nodes;
|
||||
this.prepareGraph();
|
||||
}
|
||||
|
||||
/** Prepares graph for layout calculations.
|
||||
*
|
||||
* Assumes that nodes are already topologically sorted.
|
||||
* 1. Adds nodes to graph.
|
||||
* 2. Adds elementary edges to graph.
|
||||
* 3. Splits non-elementary edges via virtual nodes.
|
||||
*/
|
||||
private prepareGraph(): void {
|
||||
this.nodes.forEach(node => {
|
||||
if (this.maxRank < node.data.rank) {
|
||||
this.maxRank = node.data.rank;
|
||||
}
|
||||
const nodeID = node.data.id;
|
||||
this.ranks.set(nodeID, node.data.rank);
|
||||
this.graph.addNode(nodeID);
|
||||
if (node.data.parents.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visited = new Set<number>();
|
||||
node.data.parents.forEach(parent => {
|
||||
if (!visited.has(parent)) {
|
||||
visited.add(parent);
|
||||
let target = nodeID;
|
||||
let currentRank = node.data.rank;
|
||||
const parentRank = this.ranks.get(parent)!;
|
||||
while (currentRank - 1 > parentRank) {
|
||||
currentRank = currentRank - 1;
|
||||
|
||||
this.virtualCount = this.virtualCount + 1;
|
||||
this.ranks.set(-this.virtualCount, currentRank);
|
||||
this.graph.addEdge(-this.virtualCount, target);
|
||||
target = -this.virtualCount;
|
||||
}
|
||||
this.graph.addEdge(parent, target);
|
||||
}
|
||||
export function applyLayout(nodes: Node<TMGraphNode>[], edges: Edge[]) {
|
||||
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
dagreGraph.setGraph({
|
||||
rankdir: 'BT',
|
||||
ranksep: VERT_SEPARATION,
|
||||
nodesep: HOR_SEPARATION,
|
||||
ranker: 'network-simplex',
|
||||
align: undefined
|
||||
});
|
||||
nodes.forEach(node => {
|
||||
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||
});
|
||||
|
||||
edges.forEach(edge => {
|
||||
dagreGraph.setEdge(edge.source, edge.target, { weight: edge.data ? CARTESIAN_WEIGHT : BOOLEAN_WEIGHT });
|
||||
});
|
||||
|
||||
dagre.layout(dagreGraph);
|
||||
|
||||
nodes.forEach(node => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id);
|
||||
node.position.x = nodeWithPosition.x - NODE_WIDTH / 2;
|
||||
node.position.y = nodeWithPosition.y - NODE_HEIGHT / 2;
|
||||
});
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.calculateLayers();
|
||||
this.calculatePositions();
|
||||
this.savePositions();
|
||||
}
|
||||
|
||||
private calculateLayers(): void {
|
||||
this.initLayers();
|
||||
// TODO: implement ordering algorithm iterations
|
||||
}
|
||||
|
||||
private initLayers(): void {
|
||||
this.layers = Array.from({ length: this.maxRank + 1 }, () => []);
|
||||
|
||||
const visited = new Set<number>();
|
||||
const dfs = (nodeID: number) => {
|
||||
if (visited.has(nodeID)) {
|
||||
return;
|
||||
}
|
||||
visited.add(nodeID);
|
||||
this.layers[this.ranks.get(nodeID)!].push(nodeID);
|
||||
this.graph.at(nodeID)!.outputs.forEach(dfs);
|
||||
};
|
||||
|
||||
const simpleNodes = this.nodes
|
||||
.filter(node => node.data.rank === 0)
|
||||
.sort((a, b) => a.data.text.localeCompare(b.data.text))
|
||||
.map(node => node.data.id);
|
||||
|
||||
simpleNodes.forEach(dfs);
|
||||
}
|
||||
|
||||
private calculatePositions(): void {
|
||||
this.initPositions();
|
||||
|
||||
for (let i = 0; i < LAYOUT_ITERATIONS; i++) {
|
||||
this.fixLayersPositions();
|
||||
}
|
||||
}
|
||||
|
||||
private fixLayersPositions(): void {
|
||||
for (let rank = 1; rank <= this.maxRank; rank++) {
|
||||
this.layers[rank].reverse().forEach(nodeID => {
|
||||
const inputs = this.graph.at(nodeID)!.inputs;
|
||||
const currentPos = this.posX.get(nodeID)!;
|
||||
if (inputs.length === 1) {
|
||||
const parent = inputs[0];
|
||||
const parentPos = this.posX.get(parent)!;
|
||||
if (currentPos === parentPos) {
|
||||
return;
|
||||
}
|
||||
if (currentPos > parentPos) {
|
||||
this.tryMoveNodeX(parent, currentPos);
|
||||
} else {
|
||||
this.tryMoveNodeX(nodeID, parentPos);
|
||||
}
|
||||
} else if (inputs.length % 2 === 1) {
|
||||
const median = inputs[Math.floor(inputs.length / 2)];
|
||||
const medianPos = this.posX.get(median)!;
|
||||
if (currentPos === medianPos) {
|
||||
return;
|
||||
}
|
||||
this.tryMoveNodeX(nodeID, medianPos);
|
||||
} else {
|
||||
const median1 = inputs[Math.floor(inputs.length / 2)];
|
||||
const median2 = inputs[Math.floor(inputs.length / 2) - 1];
|
||||
const medianPos = (this.posX.get(median1)! + this.posX.get(median2)!) / 2;
|
||||
this.tryMoveNodeX(nodeID, medianPos);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private tryMoveNodeX(nodeID: number, targetX: number) {
|
||||
const rank = this.ranks.get(nodeID)!;
|
||||
if (this.layers[rank].some(id => id !== nodeID && Math.abs(targetX - this.posX.get(id)!) < MIN_NODE_DISTANCE)) {
|
||||
return;
|
||||
}
|
||||
this.posX.set(nodeID, targetX);
|
||||
}
|
||||
|
||||
private initPositions(): void {
|
||||
this.layers.forEach((layer, rank) => {
|
||||
layer.forEach((nodeID, index) => {
|
||||
this.posX.set(nodeID, index * UNIT_WIDTH);
|
||||
this.posY.set(nodeID, -rank * UNIT_HEIGHT);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private savePositions(): void {
|
||||
this.nodes.forEach(node => {
|
||||
const nodeID = node.data.id;
|
||||
node.position = {
|
||||
x: this.posX.get(nodeID)!,
|
||||
y: this.posY.get(nodeID)!
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,21 @@ import { useMemo } from 'react';
|
|||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { MGraphNodeInternal } from '@/models/miscellaneous';
|
||||
import { TMGraphNode } from '@/models/TMGraph';
|
||||
import { colorBgTMGraphNode } from '@/styling/color';
|
||||
import { globals } from '@/utils/constants';
|
||||
|
||||
/**
|
||||
* Represents graph TMGraph node internal data.
|
||||
*/
|
||||
interface MGraphNodeInternal {
|
||||
id: string;
|
||||
data: TMGraphNode;
|
||||
dragging: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}
|
||||
|
||||
function MGraphNode(node: MGraphNodeInternal) {
|
||||
const { colors } = useConceptOptions();
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import { EdgeProps, Node } from 'reactflow';
|
|||
|
||||
import { LibraryItemType, LocationHead } from './library';
|
||||
import { IOperation } from './oss';
|
||||
import { TMGraphNode } from './TMGraph';
|
||||
import { UserID } from './user';
|
||||
|
||||
/**
|
||||
|
@ -46,17 +45,6 @@ export interface OssNodeInternal {
|
|||
yPos: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents graph TMGraph node internal data.
|
||||
*/
|
||||
export interface MGraphNodeInternal {
|
||||
id: string;
|
||||
data: TMGraphNode;
|
||||
dragging: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents graph TMGraph edge internal data.
|
||||
*/
|
||||
|
|
|
@ -150,8 +150,8 @@ function LibraryPage() {
|
|||
const viewLocations = useMemo(
|
||||
() => (
|
||||
<ViewSideLocation
|
||||
active={options.location}
|
||||
setActive={options.setLocation}
|
||||
activeLocation={options.location}
|
||||
onChangeActiveLocation={options.setLocation}
|
||||
subfolders={subfolders}
|
||||
folderTree={library.folders}
|
||||
toggleFolderMode={toggleFolderMode}
|
||||
|
@ -200,11 +200,11 @@ function LibraryPage() {
|
|||
filtered={items.length}
|
||||
hasCustomFilter={hasCustomFilter}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
onChangeQuery={setQuery}
|
||||
path={path}
|
||||
setPath={setPath}
|
||||
onChangePath={setPath}
|
||||
head={head}
|
||||
setHead={setHead}
|
||||
onChangeHead={setHead}
|
||||
isVisible={isVisible}
|
||||
isOwned={isOwned}
|
||||
toggleOwned={toggleOwned}
|
||||
|
@ -212,7 +212,7 @@ function LibraryPage() {
|
|||
isEditor={isEditor}
|
||||
toggleEditor={toggleEditor}
|
||||
filterUser={filterUser}
|
||||
setFilterUser={setFilterUser}
|
||||
onChangeFilterUser={setFilterUser}
|
||||
resetFilter={resetFilter}
|
||||
folderMode={options.folderMode}
|
||||
toggleFolderMode={toggleFolderMode}
|
||||
|
|
|
@ -36,11 +36,11 @@ interface ToolbarSearchProps {
|
|||
hasCustomFilter: boolean;
|
||||
|
||||
query: string;
|
||||
setQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangeQuery: (newValue: string) => void;
|
||||
path: string;
|
||||
setPath: React.Dispatch<React.SetStateAction<string>>;
|
||||
onChangePath: (newValue: string) => void;
|
||||
head: LocationHead | undefined;
|
||||
setHead: React.Dispatch<React.SetStateAction<LocationHead | undefined>>;
|
||||
onChangeHead: (newValue: LocationHead | undefined) => void;
|
||||
|
||||
folderMode: boolean;
|
||||
toggleFolderMode: () => void;
|
||||
|
@ -52,7 +52,7 @@ interface ToolbarSearchProps {
|
|||
isEditor: boolean | undefined;
|
||||
toggleEditor: () => void;
|
||||
filterUser: UserID | undefined;
|
||||
setFilterUser: React.Dispatch<React.SetStateAction<UserID | undefined>>;
|
||||
onChangeFilterUser: (newValue: UserID | undefined) => void;
|
||||
|
||||
resetFilter: () => void;
|
||||
}
|
||||
|
@ -63,11 +63,11 @@ function ToolbarSearch({
|
|||
hasCustomFilter,
|
||||
|
||||
query,
|
||||
setQuery,
|
||||
onChangeQuery,
|
||||
path,
|
||||
setPath,
|
||||
onChangePath,
|
||||
head,
|
||||
setHead,
|
||||
onChangeHead,
|
||||
|
||||
folderMode,
|
||||
toggleFolderMode,
|
||||
|
@ -79,7 +79,7 @@ function ToolbarSearch({
|
|||
isEditor,
|
||||
toggleEditor,
|
||||
filterUser,
|
||||
setFilterUser,
|
||||
onChangeFilterUser,
|
||||
|
||||
resetFilter
|
||||
}: ToolbarSearchProps) {
|
||||
|
@ -95,9 +95,9 @@ function ToolbarSearch({
|
|||
const handleChange = useCallback(
|
||||
(newValue: LocationHead | undefined) => {
|
||||
headMenu.hide();
|
||||
setHead(newValue);
|
||||
onChangeHead(newValue);
|
||||
},
|
||||
[headMenu, setHead]
|
||||
[headMenu, onChangeHead]
|
||||
);
|
||||
|
||||
const handleToggleFolder = useCallback(() => {
|
||||
|
@ -170,7 +170,7 @@ function ToolbarSearch({
|
|||
className='min-w-[15rem] text-sm'
|
||||
items={users}
|
||||
value={filterUser}
|
||||
onSelectValue={setFilterUser}
|
||||
onSelectValue={onChangeFilterUser}
|
||||
/>
|
||||
</motion.div>
|
||||
</Dropdown>
|
||||
|
@ -190,8 +190,8 @@ function ToolbarSearch({
|
|||
placeholder='Поиск'
|
||||
noBorder
|
||||
className={clsx('min-w-[7rem] sm:min-w-[10rem] max-w-[20rem]', folderMode && 'flex-grow')}
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
query={query}
|
||||
onChangeQuery={onChangeQuery}
|
||||
/>
|
||||
{!folderMode ? (
|
||||
<div ref={headMenu.ref} className='flex items-center h-full py-1 select-none'>
|
||||
|
@ -249,8 +249,8 @@ function ToolbarSearch({
|
|||
noIcon
|
||||
noBorder
|
||||
className='w-[4.5rem] sm:w-[5rem] flex-grow'
|
||||
value={path}
|
||||
onChange={setPath}
|
||||
query={path}
|
||||
onChangeQuery={onChangePath}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
@ -22,8 +22,8 @@ import { information } from '@/utils/labels';
|
|||
interface ViewSideLocationProps {
|
||||
folderTree: FolderTree;
|
||||
subfolders: boolean;
|
||||
active: string;
|
||||
setActive: React.Dispatch<React.SetStateAction<string>>;
|
||||
activeLocation: string;
|
||||
onChangeActiveLocation: (newValue: string) => void;
|
||||
toggleFolderMode: () => void;
|
||||
toggleSubfolders: () => void;
|
||||
onRenameLocation: () => void;
|
||||
|
@ -31,9 +31,9 @@ interface ViewSideLocationProps {
|
|||
|
||||
function ViewSideLocation({
|
||||
folderTree,
|
||||
active,
|
||||
activeLocation,
|
||||
subfolders,
|
||||
setActive: setActive,
|
||||
onChangeActiveLocation,
|
||||
toggleFolderMode,
|
||||
toggleSubfolders,
|
||||
onRenameLocation
|
||||
|
@ -44,16 +44,18 @@ function ViewSideLocation({
|
|||
const windowSize = useWindowSize();
|
||||
|
||||
const canRename = useMemo(() => {
|
||||
if (active.length <= 3 || !user) {
|
||||
if (activeLocation.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}/`));
|
||||
const located = owned.filter(
|
||||
item => item.location == activeLocation || item.location.startsWith(`${activeLocation}/`)
|
||||
);
|
||||
return located.length !== 0;
|
||||
}, [active, user, items]);
|
||||
}, [activeLocation, user, items]);
|
||||
|
||||
const animations = useMemo(() => animateSideMinWidth(windowSize.isSmall ? '10rem' : '15rem'), [windowSize]);
|
||||
const maxHeight = useMemo(() => calculateHeight('4.5rem'), [calculateHeight]);
|
||||
|
@ -68,10 +70,10 @@ function ViewSideLocation({
|
|||
.then(() => toast.success(information.pathReady))
|
||||
.catch(console.error);
|
||||
} else {
|
||||
setActive(target.getPath());
|
||||
onChangeActiveLocation(target.getPath());
|
||||
}
|
||||
},
|
||||
[setActive]
|
||||
[onChangeActiveLocation]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -109,7 +111,7 @@ function ViewSideLocation({
|
|||
</div>
|
||||
</div>
|
||||
<SelectLocation
|
||||
value={active}
|
||||
value={activeLocation}
|
||||
folderTree={folderTree}
|
||||
prefix={prefixes.folders_list}
|
||||
onClick={handleClickFolder}
|
||||
|
|
|
@ -69,7 +69,7 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
|
|||
<SelectTree
|
||||
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
||||
value={activeTopic}
|
||||
setValue={handleSelectTopic}
|
||||
onChangeValue={handleSelectTopic}
|
||||
prefix={prefixes.topic_list}
|
||||
getParent={item => topicParent.get(item) ?? item}
|
||||
getLabel={labelHelpTopic}
|
||||
|
|
|
@ -17,7 +17,7 @@ function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
|
|||
<SelectTree
|
||||
items={Object.values(HelpTopic).map(item => item as HelpTopic)}
|
||||
value={activeTopic}
|
||||
setValue={onChangeTopic}
|
||||
onChangeValue={onChangeTopic}
|
||||
prefix={prefixes.topic_list}
|
||||
getParent={item => topicParent.get(item) ?? item}
|
||||
getLabel={labelHelpTopic}
|
||||
|
|
|
@ -28,7 +28,7 @@ function HelpFormulaTree() {
|
|||
<span style={{ backgroundColor: colors.bgRed }}>присвоение и итерация</span>
|
||||
</li>
|
||||
<li>
|
||||
<span style={{ backgroundColor: '#7ca0ab' }}>составные выражения</span>
|
||||
<span style={{ backgroundColor: colors.bgDisabled }}>составные выражения</span>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -15,7 +15,8 @@ import {
|
|||
IconSave,
|
||||
IconSettings,
|
||||
IconStatusOK,
|
||||
IconTree
|
||||
IconTree,
|
||||
IconTypeGraph
|
||||
} from '@/components/Icons';
|
||||
import LinkTopic from '@/components/ui/LinkTopic';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
|
@ -90,6 +91,10 @@ function HelpRSEditor() {
|
|||
<li>
|
||||
<IconControls className='inline-icon' /> специальная клавиатура и горячие клавиши
|
||||
</li>
|
||||
<li>
|
||||
<IconTypeGraph className='inline-icon' /> отображение{' '}
|
||||
<LinkTopic text='графа ступеней типизации' topic={HelpTopic.UI_TYPE_GRAPH} />
|
||||
</li>
|
||||
<li>
|
||||
<IconTree className='inline-icon' /> отображение{' '}
|
||||
<LinkTopic text='дерева разбора' topic={HelpTopic.UI_FORMULA_TREE} />
|
||||
|
|
|
@ -15,7 +15,7 @@ import OssStats from './OssStats';
|
|||
|
||||
interface EditorOssCardProps {
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
onDestroy: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import { useOssEdit } from '../OssEditContext';
|
|||
interface FormOSSProps {
|
||||
id?: string;
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import OssFlow from './OssFlow';
|
|||
|
||||
interface EditorOssGraphProps {
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
|
||||
|
|
|
@ -34,7 +34,7 @@ import ToolbarOssGraph from './ToolbarOssGraph';
|
|||
|
||||
interface OssFlowProps {
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||
|
|
|
@ -55,7 +55,7 @@ export interface IOssEditContext extends ILibraryItemEditor {
|
|||
isAttachedToOSS: boolean;
|
||||
|
||||
showTooltip: boolean;
|
||||
setShowTooltip: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setShowTooltip: (newValue: boolean) => void;
|
||||
|
||||
setOwner: (newOwner: UserID) => void;
|
||||
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
||||
|
|
|
@ -21,7 +21,7 @@ const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
|
|||
interface EditorConstituentaProps {
|
||||
activeCst?: IConstituenta;
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ interface FormConstituentaProps {
|
|||
|
||||
isModified: boolean;
|
||||
toggleReset: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
|
||||
onRename: () => void;
|
||||
onEditTerm: () => void;
|
||||
|
@ -144,7 +144,7 @@ function FormConstituenta({
|
|||
cstUpdate(data, () => toast.success(information.changesSaved));
|
||||
}
|
||||
|
||||
function handleTypificationClick(event: CProps.EventMouse) {
|
||||
function handleTypeGraph(event: CProps.EventMouse) {
|
||||
if (!state || (localParse && !localParse.parseResult) || state.parse.status !== ParsingStatus.VERIFIED) {
|
||||
toast.error(errors.typeStructureFailed);
|
||||
return;
|
||||
|
@ -196,10 +196,8 @@ function FormConstituenta({
|
|||
noOutline
|
||||
readOnly
|
||||
label='Типизация'
|
||||
title='Отобразить структуру типизации'
|
||||
value={typification}
|
||||
colors='clr-app clr-text-default cursor-pointer'
|
||||
onClick={event => handleTypificationClick(event)}
|
||||
colors='clr-app clr-text-default cursor-default'
|
||||
/>
|
||||
) : null}
|
||||
{state ? (
|
||||
|
@ -225,10 +223,11 @@ function FormConstituenta({
|
|||
activeCst={state}
|
||||
disabled={disabled || state.is_inherited}
|
||||
toggleReset={toggleReset}
|
||||
onChange={newValue => setExpression(newValue)}
|
||||
setTypification={setTypification}
|
||||
setLocalParse={setLocalParse}
|
||||
onChangeExpression={newValue => setExpression(newValue)}
|
||||
onChangeTypification={setTypification}
|
||||
onChangeLocalParse={setLocalParse}
|
||||
onOpenEdit={onOpenEdit}
|
||||
onShowTypeGraph={handleTypeGraph}
|
||||
/>
|
||||
</AnimateFade>
|
||||
<AnimateFade key='cst_definition_fade' hideContent={!state.definition_raw && isElementary}>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
|
@ -39,10 +39,11 @@ interface EditorRSExpressionProps {
|
|||
disabled?: boolean;
|
||||
toggleReset?: boolean;
|
||||
|
||||
setTypification: (typification: string) => void;
|
||||
setLocalParse: React.Dispatch<React.SetStateAction<IExpressionParse | undefined>>;
|
||||
onChange: (newValue: string) => void;
|
||||
onChangeTypification: (typification: string) => void;
|
||||
onChangeLocalParse: (typification: IExpressionParse | undefined) => void;
|
||||
onChangeExpression: (newValue: string) => void;
|
||||
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||
onShowTypeGraph: (event: CProps.EventMouse) => void;
|
||||
}
|
||||
|
||||
function EditorRSExpression({
|
||||
|
@ -50,10 +51,11 @@ function EditorRSExpression({
|
|||
disabled,
|
||||
value,
|
||||
toggleReset,
|
||||
setTypification,
|
||||
setLocalParse,
|
||||
onChange,
|
||||
onChangeTypification,
|
||||
onChangeLocalParse,
|
||||
onChangeExpression,
|
||||
onOpenEdit,
|
||||
onShowTypeGraph,
|
||||
...restProps
|
||||
}: EditorRSExpressionProps) {
|
||||
const model = useRSForm();
|
||||
|
@ -74,20 +76,20 @@ function EditorRSExpression({
|
|||
}, [activeCst, resetParse, toggleReset]);
|
||||
|
||||
function handleChange(newValue: string) {
|
||||
onChange(newValue);
|
||||
onChangeExpression(newValue);
|
||||
setIsModified(newValue !== activeCst.definition_formal);
|
||||
}
|
||||
|
||||
function handleCheckExpression(callback?: (parse: IExpressionParse) => void) {
|
||||
parser.checkConstituenta(value, activeCst, parse => {
|
||||
setLocalParse(parse);
|
||||
onChangeLocalParse(parse);
|
||||
if (parse.errors.length > 0) {
|
||||
onShowError(parse.errors[0], parse.prefixLen);
|
||||
} else {
|
||||
rsInput.current?.view?.focus();
|
||||
}
|
||||
setIsModified(false);
|
||||
setTypification(
|
||||
onChangeTypification(
|
||||
labelTypification({
|
||||
isValid: parse.parseResult,
|
||||
resultType: parse.typification,
|
||||
|
@ -171,6 +173,7 @@ function EditorRSExpression({
|
|||
showControls={showControls}
|
||||
showAST={handleShowAST}
|
||||
toggleControls={() => setShowControls(prev => !prev)}
|
||||
showTypeGraph={onShowTypeGraph}
|
||||
/>
|
||||
|
||||
<Overlay
|
||||
|
|
|
@ -54,7 +54,7 @@ function StatusBar({ isModified, processing, activeCst, parseData, onAnalyze }:
|
|||
onClick={onAnalyze}
|
||||
>
|
||||
<AnimatePresence mode='wait'>
|
||||
{processing ? <Loader key='status-loader' size={3} /> : null}
|
||||
{processing ? <Loader key='status-loader' scale={3} /> : null}
|
||||
{!processing ? (
|
||||
<>
|
||||
<StatusIcon key='status-icon' size='1rem' value={status} />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IconControls, IconTree } from '@/components/Icons';
|
||||
import { IconControls, IconTree, IconTypeGraph } from '@/components/Icons';
|
||||
import { CProps } from '@/components/props';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
|
@ -10,9 +10,16 @@ interface ToolbarRSExpressionProps {
|
|||
|
||||
toggleControls: () => void;
|
||||
showAST: (event: CProps.EventMouse) => void;
|
||||
showTypeGraph: (event: CProps.EventMouse) => void;
|
||||
}
|
||||
|
||||
function ToolbarRSExpression({ disabled, showControls, toggleControls, showAST }: ToolbarRSExpressionProps) {
|
||||
function ToolbarRSExpression({
|
||||
disabled,
|
||||
showControls,
|
||||
showTypeGraph,
|
||||
toggleControls,
|
||||
showAST
|
||||
}: ToolbarRSExpressionProps) {
|
||||
const model = useRSForm();
|
||||
|
||||
return (
|
||||
|
@ -24,6 +31,11 @@ function ToolbarRSExpression({ disabled, showControls, toggleControls, showAST }
|
|||
onClick={toggleControls}
|
||||
/>
|
||||
) : null}
|
||||
<MiniButton
|
||||
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
|
||||
title='Граф ступеней типизации'
|
||||
onClick={showTypeGraph}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Дерево разбора выражения'
|
||||
onClick={showAST}
|
||||
|
|
|
@ -15,7 +15,7 @@ import ToolbarRSFormCard from './ToolbarRSFormCard';
|
|||
|
||||
interface EditorRSFormCardProps {
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
onDestroy: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import ToolbarVersioning from './ToolbarVersioning';
|
|||
interface FormRSFormProps {
|
||||
id?: string;
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setIsModified: (newValue: boolean) => void;
|
||||
}
|
||||
|
||||
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||
|
|
|
@ -152,8 +152,8 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|||
id='constituents_search'
|
||||
noBorder
|
||||
className='w-[8rem]'
|
||||
value={filterText}
|
||||
onChange={setFilterText}
|
||||
query={filterText}
|
||||
onChangeQuery={setFilterText}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
@ -74,8 +74,8 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
|
|||
id='constituents_search'
|
||||
noBorder
|
||||
className='min-w-[6rem] pr-2 flex-grow'
|
||||
value={filterText}
|
||||
onChange={setFilterText}
|
||||
query={filterText}
|
||||
onChangeQuery={setFilterText}
|
||||
/>
|
||||
{selectMatchMode}
|
||||
{selectGraph}
|
||||
|
|
|
@ -371,7 +371,7 @@ export function colorBgSyntaxTree(node: ISyntaxTreeNode, colors: IColorTheme): s
|
|||
case TokenID.NT_FUNC_CALL:
|
||||
case TokenID.NT_ARGUMENTS:
|
||||
case TokenID.NT_RECURSIVE_SHORT:
|
||||
return '';
|
||||
return colors.bgDisabled;
|
||||
|
||||
case TokenID.ASSIGN:
|
||||
case TokenID.ITERATE:
|
||||
|
|
|
@ -126,7 +126,8 @@
|
|||
height: 40px;
|
||||
}
|
||||
|
||||
.react-flow__node-step {
|
||||
.react-flow__node-step,
|
||||
.react-flow__node-token {
|
||||
cursor: default;
|
||||
|
||||
border-radius: 100%;
|
||||
|
|
|
@ -581,16 +581,16 @@ export function labelSyntaxTree(node: ISyntaxTreeNode): string {
|
|||
case TokenID.NT_TUPLE: return 'TUPLE';
|
||||
case TokenID.NT_ENUMERATION: return 'ENUM';
|
||||
|
||||
case TokenID.NT_ENUM_DECL: return 'ENUM_DECLARATION';
|
||||
case TokenID.NT_TUPLE_DECL: return 'TUPLE_DECLARATION';
|
||||
case TokenID.NT_ENUM_DECL: return 'ENUM_DECLARE';
|
||||
case TokenID.NT_TUPLE_DECL: return 'TUPLE_DECLARE';
|
||||
case TokenID.PUNCTUATION_DEFINE: return 'DEFINITION';
|
||||
case TokenID.PUNCTUATION_STRUCT: return 'STRUCTURE_DEFINITION';
|
||||
case TokenID.PUNCTUATION_STRUCT: return 'STRUCTURE_DEFINE';
|
||||
|
||||
case TokenID.NT_ARG_DECL: return 'ARG';
|
||||
case TokenID.NT_FUNC_CALL: return 'CALL';
|
||||
case TokenID.NT_ARGUMENTS: return 'ARGS';
|
||||
|
||||
case TokenID.NT_FUNC_DEFINITION: return 'FUNCTION_DEFINITION';
|
||||
case TokenID.NT_FUNC_DEFINITION: return 'FUNCTION_DEFINE';
|
||||
|
||||
case TokenID.NT_RECURSIVE_SHORT: return labelToken(TokenID.NT_RECURSIVE_FULL);
|
||||
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
# Run coverage analysis
|
||||
# Run linters
|
||||
|
||||
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
||||
$frontend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\frontend"
|
||||
|
||||
function RunLinters() {
|
||||
BackendLint
|
||||
LintBackend
|
||||
LintFrontend
|
||||
}
|
||||
|
||||
function BackendLint() {
|
||||
function LintBackend() {
|
||||
$pylint = "$backend\venv\Scripts\pylint.exe"
|
||||
$mypy = "$backend\venv\Scripts\mypy.exe"
|
||||
|
||||
|
@ -15,4 +18,9 @@ function BackendLint() {
|
|||
& $mypy project apps
|
||||
}
|
||||
|
||||
function LintFrontend() {
|
||||
Set-Location $frontend
|
||||
& npm run lint
|
||||
}
|
||||
|
||||
RunLinters
|
Loading…
Reference in New Issue
Block a user