M: Implement RSForm sorting for OSS and small UI fixes

This commit is contained in:
Ivan 2024-08-17 22:30:49 +03:00
parent 259259ec7e
commit 85c3027d3d
8 changed files with 84 additions and 31 deletions

View File

@ -4,9 +4,8 @@ import { useIntl } from 'react-intl';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { ILibraryFilter } from '@/models/miscellaneous'; import { matchLibraryItem } from '@/models/libraryAPI';
import FlexColumn from '../ui/FlexColumn'; import FlexColumn from '../ui/FlexColumn';
@ -15,6 +14,8 @@ interface PickSchemaProps {
initialFilter?: string; initialFilter?: string;
rows?: number; rows?: number;
items: ILibraryItem[];
itemType: LibraryItemType;
value?: LibraryItemID; value?: LibraryItemID;
baseFilter?: (target: ILibraryItem) => boolean; baseFilter?: (target: ILibraryItem) => boolean;
onSelectValue: (newValue: LibraryItemID) => void; onSelectValue: (newValue: LibraryItemID) => void;
@ -22,31 +23,31 @@ interface PickSchemaProps {
const columnHelper = createColumnHelper<ILibraryItem>(); const columnHelper = createColumnHelper<ILibraryItem>();
function PickSchema({ id, initialFilter = '', rows = 4, value, onSelectValue, baseFilter }: PickSchemaProps) { function PickSchema({
id,
initialFilter = '',
rows = 4,
items,
itemType,
value,
onSelectValue,
baseFilter
}: PickSchemaProps) {
const intl = useIntl(); const intl = useIntl();
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const library = useLibrary();
const [filterText, setFilterText] = useState(initialFilter); const [filterText, setFilterText] = useState(initialFilter);
const [filter, setFilter] = useState<ILibraryFilter>({}); const [filtered, setFiltered] = useState<ILibraryItem[]>([]);
const [items, setItems] = useState<ILibraryItem[]>([]); const baseFiltered = useMemo(
() => items.filter(item => item.item_type === itemType && (!baseFilter || baseFilter(item))),
[items, itemType, baseFilter]
);
useLayoutEffect(() => { useLayoutEffect(() => {
setFilter({ const newFiltered = baseFiltered.filter(item => matchLibraryItem(item, filterText));
query: filterText, setFiltered(newFiltered);
type: LibraryItemType.RSFORM
});
}, [filterText]); }, [filterText]);
useLayoutEffect(() => {
const filtered = library.applyFilter(filter);
if (baseFilter) {
setItems(filtered.filter(baseFilter));
} else {
setItems(filtered);
}
}, [library, filter, filter.query, baseFilter]);
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor('alias', { columnHelper.accessor('alias', {
@ -106,7 +107,7 @@ function PickSchema({ id, initialFilter = '', rows = 4, value, onSelectValue, ba
noHeader noHeader
noFooter noFooter
className='text-sm select-none cc-scroll-y' className='text-sm select-none cc-scroll-y'
data={items} data={filtered}
columns={columns} columns={columns}
conditionalRowStyles={conditionalRowStyles} conditionalRowStyles={conditionalRowStyles}
noDataComponent={ noDataComponent={

View File

@ -8,8 +8,10 @@ import PickSchema from '@/components/select/PickSchema';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import { ILibraryItem, LibraryItemID } from '@/models/library'; import { useLibrary } from '@/context/LibraryContext';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { IOperation, IOperationSchema } from '@/models/oss'; import { IOperation, IOperationSchema } from '@/models/oss';
import { sortItemsForOSS } from '@/models/ossAPI';
interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> { interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> {
oss: IOperationSchema; oss: IOperationSchema;
@ -19,6 +21,8 @@ interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> {
function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeInputSchemaProps) { function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeInputSchemaProps) {
const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined); const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined);
const library = useLibrary();
const sortedItems = useMemo(() => sortItemsForOSS(oss, library.items), [oss, library.items]);
const baseFilter = useCallback( const baseFilter = useCallback(
(item: ILibraryItem) => !oss.schemas.includes(item.id) || item.id === selected || item.id === target.result, (item: ILibraryItem) => !oss.schemas.includes(item.id) || item.id === selected || item.id === target.result,
@ -55,6 +59,8 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn
</div> </div>
</div> </div>
<PickSchema <PickSchema
items={sortedItems}
itemType={LibraryItemType.RSFORM}
value={selected} // prettier: split-line value={selected} // prettier: split-line
onSelectValue={handleSelectLocation} onSelectValue={handleSelectLocation}
rows={8} rows={8}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { IconReset } from '@/components/Icons'; import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
@ -10,8 +10,10 @@ import MiniButton from '@/components/ui/MiniButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { ILibraryItem, LibraryItemID } from '@/models/library'; import { useLibrary } from '@/context/LibraryContext';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { IOperationSchema } from '@/models/oss'; import { IOperationSchema } from '@/models/oss';
import { sortItemsForOSS } from '@/models/ossAPI';
import { limits, patterns } from '@/utils/constants'; import { limits, patterns } from '@/utils/constants';
interface TabInputOperationProps { interface TabInputOperationProps {
@ -42,6 +44,8 @@ function TabInputOperation({
setCreateSchema setCreateSchema
}: TabInputOperationProps) { }: TabInputOperationProps) {
const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]); const baseFilter = useCallback((item: ILibraryItem) => !oss.schemas.includes(item.id), [oss]);
const library = useLibrary();
const sortedItems = useMemo(() => sortItemsForOSS(oss, library.items), [oss, library.items]);
useEffect(() => { useEffect(() => {
if (createSchema) { if (createSchema) {
@ -102,7 +106,9 @@ function TabInputOperation({
</div> </div>
{!createSchema ? ( {!createSchema ? (
<PickSchema <PickSchema
value={attachedID} // prettier: split-line items={sortedItems}
value={attachedID}
itemType={LibraryItemType.RSFORM}
onSelectValue={setAttachedID} onSelectValue={setAttachedID}
rows={8} rows={8}
baseFilter={baseFilter} baseFilter={baseFilter}

View File

@ -6,7 +6,7 @@ import PickSchema from '@/components/select/PickSchema';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID, LibraryItemType } from '@/models/library';
interface TabSchemaProps { interface TabSchemaProps {
selected?: LibraryItemID; selected?: LibraryItemID;
@ -33,6 +33,8 @@ function TabSchema({ selected, setSelected }: TabSchemaProps) {
</div> </div>
<PickSchema <PickSchema
id='dlg_schema_picker' // prettier: split lines id='dlg_schema_picker' // prettier: split lines
items={library.items}
itemType={LibraryItemType.RSFORM}
rows={15} rows={15}
value={selected} value={selected}
onSelectValue={setSelected} onSelectValue={setSelected}

View File

@ -4,7 +4,8 @@
import { TextMatcher } from '@/utils/utils'; import { TextMatcher } from '@/utils/utils';
import { IOperation } from './oss'; import { ILibraryItem } from './library';
import { IOperation, IOperationSchema } from './oss';
/** /**
* Checks if a given target {@link IOperation} matches the specified query using. * Checks if a given target {@link IOperation} matches the specified query using.
@ -16,3 +17,29 @@ export function matchOperation(target: IOperation, query: string): boolean {
const matcher = new TextMatcher(query); const matcher = new TextMatcher(query);
return matcher.test(target.alias) || matcher.test(target.title); return matcher.test(target.alias) || matcher.test(target.title);
} }
/**
* Sorts library items relevant for the specified {@link IOperationSchema}.
*
* @param oss - The {@link IOperationSchema} to be sorted.
* @param items - The items to be sorted.
*/
export function sortItemsForOSS(oss: IOperationSchema, items: ILibraryItem[]): ILibraryItem[] {
const result = items.filter(item => item.location === oss.location);
for (const item of items) {
if (item.visible && item.owner === oss.owner && !result.includes(item)) {
result.push(item);
}
}
for (const item of result) {
if (item.visible && !result.includes(item)) {
result.push(item);
}
}
for (const item of result) {
if (!result.includes(item)) {
result.push(item);
}
}
return result;
}

View File

@ -113,8 +113,8 @@ function NodeContextMenu({
{operation.result ? ( {operation.result ? (
<DropdownButton <DropdownButton
text={prepareTooltip('Открыть схему', 'Двойной клик')} text='Открыть схему'
title='Открыть привязанную КС' titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')}
icon={<IconRSForm size='1rem' className='icon-green' />} icon={<IconRSForm size='1rem' className='icon-green' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={handleOpenSchema} onClick={handleOpenSchema}

View File

@ -8,6 +8,7 @@ import { urls } from '@/app/urls';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema'; import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema';
@ -30,7 +31,7 @@ import {
} from '@/models/oss'; } from '@/models/oss';
import { UserID, UserLevel } from '@/models/user'; import { UserID, UserLevel } from '@/models/user';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { information } from '@/utils/labels'; import { errors, information } from '@/utils/labels';
export interface ICreateOperationPrompt { export interface ICreateOperationPrompt {
x: number; x: number;
@ -95,6 +96,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
const { adminMode } = useConceptOptions(); const { adminMode } = useConceptOptions();
const { accessLevel, setAccessLevel } = useAccessMode(); const { accessLevel, setAccessLevel } = useAccessMode();
const model = useOSS(); const model = useOSS();
const library = useLibrary();
const isMutable = useMemo( const isMutable = useMemo(
() => accessLevel > UserLevel.READER && !model.schema?.read_only, () => accessLevel > UserLevel.READER && !model.schema?.read_only,
@ -307,12 +309,20 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
const createInput = useCallback( const createInput = useCallback(
(target: OperationID, positions: IOperationPosition[]) => { (target: OperationID, positions: IOperationPosition[]) => {
const operation = model.schema?.operationByID.get(target);
if (!model.schema || !operation) {
return;
}
if (library.items.find(item => item.alias === operation.alias && item.location === model.schema!.location)) {
toast.error(errors.inputAlreadyExists);
return;
}
model.createInput({ target: target, positions: positions }, new_schema => { model.createInput({ target: target, positions: positions }, new_schema => {
toast.success(information.newLibraryItem); toast.success(information.newLibraryItem);
router.push(urls.schema(new_schema.id)); router.push(urls.schema(new_schema.id));
}); });
}, },
[model, router] [model, library.items, router]
); );
const promptEditInput = useCallback((target: OperationID, positions: IOperationPosition[]) => { const promptEditInput = useCallback((target: OperationID, positions: IOperationPosition[]) => {

View File

@ -953,7 +953,8 @@ export const errors = {
passwordsMismatch: 'Пароли не совпадают', passwordsMismatch: 'Пароли не совпадают',
imageFailed: 'Ошибка при создании изображения', imageFailed: 'Ошибка при создании изображения',
reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении', reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении',
substituteInherited: 'Нельзя удалять наследованные конституенты при отождествлении' substituteInherited: 'Нельзя удалять наследованные конституенты при отождествлении',
inputAlreadyExists: 'Концептуальная схема с таким именем уже существует'
}; };
/** /**