mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
R: Remove unnecessary rerenders from useEffect
This commit is contained in:
parent
ca203a1bfb
commit
6fa25b51fe
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { IconRemove } from '../icons';
|
import { IconRemove } from '../icons';
|
||||||
|
@ -49,11 +49,12 @@ export function ComboBox<Option>({
|
||||||
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
function handleOpenChange(isOpen: boolean) {
|
||||||
if (triggerRef.current) {
|
setOpen(isOpen);
|
||||||
|
if (isOpen && triggerRef.current) {
|
||||||
setPopoverWidth(triggerRef.current.offsetWidth);
|
setPopoverWidth(triggerRef.current.offsetWidth);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}
|
||||||
|
|
||||||
function handleChangeValue(newValue: Option | null) {
|
function handleChangeValue(newValue: Option | null) {
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
|
@ -66,7 +67,7 @@ export function ComboBox<Option>({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<button
|
<button
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { IconRemove } from '../icons';
|
import { IconRemove } from '../icons';
|
||||||
|
@ -43,11 +43,12 @@ export function ComboMulti<Option>({
|
||||||
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
function handleOpenChange(isOpen: boolean) {
|
||||||
if (triggerRef.current) {
|
setOpen(isOpen);
|
||||||
|
if (isOpen && triggerRef.current) {
|
||||||
setPopoverWidth(triggerRef.current.offsetWidth);
|
setPopoverWidth(triggerRef.current.offsetWidth);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}
|
||||||
|
|
||||||
function handleAddValue(newValue: Option) {
|
function handleAddValue(newValue: Option) {
|
||||||
if (value.includes(newValue)) {
|
if (value.includes(newValue)) {
|
||||||
|
@ -70,7 +71,7 @@ export function ComboMulti<Option>({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<button
|
<button
|
||||||
id={id}
|
id={id}
|
||||||
|
|
|
@ -44,11 +44,12 @@ export function SelectTree<ItemType>({
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectTreeProps<ItemType>) {
|
}: SelectTreeProps<ItemType>) {
|
||||||
const foldable = new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item)));
|
const foldable = new Set(items.filter(item => getParent(item) !== item).map(item => getParent(item)));
|
||||||
const [folded, setFolded] = useState<ItemType[]>(items);
|
const defaultFolded = items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item);
|
||||||
|
const [folded, setFolded] = useState<ItemType[]>(defaultFolded);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFolded(items.filter(item => getParent(value) !== item && getParent(getParent(value)) !== item));
|
setFolded(defaultFolded);
|
||||||
}, [value, getParent, items]);
|
}, [defaultFolded]);
|
||||||
|
|
||||||
function onFoldItem(target: ItemType) {
|
function onFoldItem(target: ItemType) {
|
||||||
setFolded(prev =>
|
setFolded(prev =>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
|
|
||||||
|
@ -13,13 +13,26 @@ import { useQueryStrings } from '@/hooks/use-query-strings';
|
||||||
|
|
||||||
import { useResetPassword } from '../backend/use-reset-password';
|
import { useResetPassword } from '../backend/use-reset-password';
|
||||||
|
|
||||||
|
function useTokenValidation(token: string, isPending: boolean) {
|
||||||
|
const { validateToken } = useResetPassword();
|
||||||
|
const [isTokenValidating, setIsTokenValidating] = useState(false);
|
||||||
|
|
||||||
|
const validate = async () => {
|
||||||
|
if (!isTokenValidating && !isPending) {
|
||||||
|
await validateToken({ token });
|
||||||
|
setIsTokenValidating(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { isTokenValidating, validate };
|
||||||
|
}
|
||||||
|
|
||||||
export function Component() {
|
export function Component() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const token = useQueryStrings().get('token') ?? '';
|
const token = useQueryStrings().get('token') ?? '';
|
||||||
|
|
||||||
const { validateToken, resetPassword, isPending, error: serverError } = useResetPassword();
|
const { resetPassword, isPending, error: serverError } = useResetPassword();
|
||||||
|
const { isTokenValidating, validate } = useTokenValidation(token, isPending);
|
||||||
|
|
||||||
const [isTokenValidating, setIsTokenValidating] = useState(false);
|
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
|
const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
|
||||||
|
|
||||||
|
@ -38,12 +51,9 @@ export function Component() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isTokenValidating && !isPending) {
|
if (!isTokenValidating && !isPending) {
|
||||||
void validateToken({ token: token });
|
void validate();
|
||||||
setIsTokenValidating(true);
|
|
||||||
}
|
}
|
||||||
}, [token, validateToken, isTokenValidating, isPending]);
|
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
|
|
|
@ -54,7 +54,7 @@ export function LibraryPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolbarSearch className='top-0 h-9' total={libraryItems.length} filtered={filtered.length} />
|
<ToolbarSearch className='top-0 h-9' total={libraryItems.length} filtered={filtered.length} />
|
||||||
<div className='relative cc-fade-in flex'>
|
<div className='relative flex'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Выгрузить в формате CSV'
|
title='Выгрузить в формате CSV'
|
||||||
className='absolute z-tooltip -top-8 right-6 hidden sm:block'
|
className='absolute z-tooltip -top-8 right-6 hidden sm:block'
|
||||||
|
|
|
@ -43,11 +43,7 @@ export function EditorOssCard() {
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
onKeyDown={handleInput}
|
onKeyDown={handleInput}
|
||||||
className={clsx(
|
className={clsx('md:max-w-fit max-w-128 min-w-fit', 'flex flex-row flex-wrap pt-8 px-6 justify-center')}
|
||||||
'cc-fade-in',
|
|
||||||
'md:max-w-fit max-w-128 min-w-fit',
|
|
||||||
'flex flex-row flex-wrap pt-8 px-6 justify-center'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div className='cc-column px-3'>
|
<div className='cc-column px-3'>
|
||||||
<FormOSS />
|
<FormOSS />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export function FormOSS() {
|
||||||
control,
|
control,
|
||||||
setValue,
|
setValue,
|
||||||
reset,
|
reset,
|
||||||
formState: { isDirty, errors }
|
formState: { errors, isDirty }
|
||||||
} = useForm<IUpdateLibraryItemDTO>({
|
} = useForm<IUpdateLibraryItemDTO>({
|
||||||
resolver: zodResolver(schemaUpdateLibraryItem),
|
resolver: zodResolver(schemaUpdateLibraryItem),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
@ -46,9 +46,11 @@ export function FormOSS() {
|
||||||
const visible = useWatch({ control, name: 'visible' });
|
const visible = useWatch({ control, name: 'visible' });
|
||||||
const readOnly = useWatch({ control, name: 'read_only' });
|
const readOnly = useWatch({ control, name: 'read_only' });
|
||||||
|
|
||||||
useEffect(() => {
|
const prevDirty = useRef(isDirty);
|
||||||
|
if (prevDirty.current !== isDirty) {
|
||||||
|
prevDirty.current = isDirty;
|
||||||
setIsModified(isDirty);
|
setIsModified(isDirty);
|
||||||
}, [isDirty, setIsModified]);
|
}
|
||||||
|
|
||||||
function onSubmit(data: IUpdateLibraryItemDTO) {
|
function onSubmit(data: IUpdateLibraryItemDTO) {
|
||||||
return updateOss(data).then(() => reset({ ...data }));
|
return updateOss(data).then(() => reset({ ...data }));
|
||||||
|
|
|
@ -411,7 +411,7 @@ export function OssFlow() {
|
||||||
<ContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
|
<ContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx('cc-fade-in relative w-[100vw] cc-mask-sides', !containMovement && 'cursor-relocate')}
|
className={clsx('relative w-[100vw] cc-mask-sides', !containMovement && 'cursor-relocate')}
|
||||||
style={{ height: mainHeight, fontFamily: 'Rubik' }}
|
style={{ height: mainHeight, fontFamily: 'Rubik' }}
|
||||||
>
|
>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
@ -8,6 +8,7 @@ import { useLibrarySearchStore } from '@/features/library';
|
||||||
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
||||||
import { RSTabID } from '@/features/rsform/pages/rsform-page/rsedit-context';
|
import { RSTabID } from '@/features/rsform/pages/rsform-page/rsedit-context';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
import { useAdjustRole } from '@/features/users/stores/use-adjust-role';
|
||||||
|
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { promptText } from '@/utils/labels';
|
import { promptText } from '@/utils/labels';
|
||||||
|
@ -27,7 +28,6 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
|
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
|
||||||
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
const searchLocation = useLibrarySearchStore(state => state.location);
|
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||||
|
|
||||||
|
@ -36,21 +36,18 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
||||||
|
|
||||||
const isOwned = !!user.id && user.id === schema.owner;
|
const isOwned = !!user.id && user.id === schema.owner;
|
||||||
const isMutable = role > UserRole.READER && !schema.read_only;
|
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||||
|
const isEditor = !!user.id && schema.editors.includes(user.id);
|
||||||
|
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
|
|
||||||
const { deleteItem } = useDeleteItem();
|
const { deleteItem } = useDeleteItem();
|
||||||
|
|
||||||
useEffect(
|
useAdjustRole({
|
||||||
() =>
|
|
||||||
adjustRole({
|
|
||||||
isOwner: isOwned,
|
isOwner: isOwned,
|
||||||
isEditor: !!user.id && schema.editors.includes(user.id),
|
isEditor: isEditor,
|
||||||
isStaff: user.is_staff,
|
isStaff: user.is_staff,
|
||||||
adminMode: adminMode
|
adminMode: adminMode
|
||||||
}),
|
});
|
||||||
[schema, adjustRole, isOwned, user, adminMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
function navigateTab(tab: OssTabID) {
|
function navigateTab(tab: OssTabID) {
|
||||||
const url = urls.oss_props({
|
const url = urls.oss_props({
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
@ -12,6 +11,7 @@ import { isAxiosError } from '@/backend/api-transport';
|
||||||
import { TextURL } from '@/components/control';
|
import { TextURL } from '@/components/control';
|
||||||
import { type ErrorData } from '@/components/info-error';
|
import { type ErrorData } from '@/components/info-error';
|
||||||
import { useQueryStrings } from '@/hooks/use-query-strings';
|
import { useQueryStrings } from '@/hooks/use-query-strings';
|
||||||
|
import { useResetModification } from '@/hooks/use-reset-modification';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
import { OperationTooltip } from '../../components/tooltip-oss-item';
|
import { OperationTooltip } from '../../components/tooltip-oss-item';
|
||||||
|
@ -26,6 +26,8 @@ const paramsSchema = z.strictObject({
|
||||||
});
|
});
|
||||||
|
|
||||||
export function OssPage() {
|
export function OssPage() {
|
||||||
|
useResetModification();
|
||||||
|
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
|
@ -35,11 +37,9 @@ export function OssPage() {
|
||||||
tab: query.get('tab')
|
tab: query.get('tab')
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isModified, setIsModified } = useModificationStore();
|
const { isModified } = useModificationStore();
|
||||||
useBlockNavigation(isModified);
|
useBlockNavigation(isModified);
|
||||||
|
|
||||||
useEffect(() => setIsModified(false), [setIsModified]);
|
|
||||||
|
|
||||||
if (!urlData.id) {
|
if (!urlData.id) {
|
||||||
router.replace({ path: urls.page404, force: true });
|
router.replace({ path: urls.page404, force: true });
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -75,7 +75,6 @@ export function EditorConstituenta() {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative ',
|
'relative ',
|
||||||
'cc-fade-in',
|
|
||||||
'min-h-80 max-w-[calc(min(100vw,80rem))] mx-auto',
|
'min-h-80 max-w-[calc(min(100vw,80rem))] mx-auto',
|
||||||
'flex pt-8',
|
'flex pt-8',
|
||||||
'overflow-y-auto overflow-x-clip',
|
'overflow-y-auto overflow-x-clip',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
@ -44,19 +44,30 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
|
||||||
const { isModified, setIsModified } = useModificationStore();
|
const { isModified, setIsModified } = useModificationStore();
|
||||||
const isProcessing = useMutatingRSForm();
|
const isProcessing = useMutatingRSForm();
|
||||||
|
|
||||||
|
const { updateConstituenta: cstUpdate } = useUpdateConstituenta();
|
||||||
|
const showTypification = useDialogsStore(state => state.showShowTypeGraph);
|
||||||
|
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
|
||||||
|
const showRenameCst = useDialogsStore(state => state.showRenameCst);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
formState: { isDirty }
|
formState: { isDirty }
|
||||||
} = useForm<IUpdateConstituentaDTO>({ resolver: zodResolver(schemaUpdateConstituenta) });
|
} = useForm<IUpdateConstituentaDTO>({
|
||||||
|
resolver: zodResolver(schemaUpdateConstituenta),
|
||||||
const { updateConstituenta: cstUpdate } = useUpdateConstituenta();
|
defaultValues: {
|
||||||
const showTypification = useDialogsStore(state => state.showShowTypeGraph);
|
target: activeCst.id,
|
||||||
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
|
item_data: {
|
||||||
const showRenameCst = useDialogsStore(state => state.showRenameCst);
|
convention: activeCst.convention,
|
||||||
|
term_raw: activeCst.term_raw,
|
||||||
|
definition_raw: activeCst.definition_raw,
|
||||||
|
definition_formal: activeCst.definition_formal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [forceComment, setForceComment] = useState(false);
|
||||||
const [localParse, setLocalParse] = useState<IExpressionParseDTO | null>(null);
|
const [localParse, setLocalParse] = useState<IExpressionParseDTO | null>(null);
|
||||||
|
|
||||||
const typification = useMemo(
|
const typification = useMemo(
|
||||||
|
@ -80,12 +91,21 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
|
||||||
[activeCst, localParse]
|
[activeCst, localParse]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [forceComment, setForceComment] = useState(false);
|
|
||||||
const isBasic = isBasicConcept(activeCst.cst_type);
|
const isBasic = isBasicConcept(activeCst.cst_type);
|
||||||
const isElementary = isBaseSet(activeCst.cst_type);
|
const isElementary = isBaseSet(activeCst.cst_type);
|
||||||
const showConvention = !!activeCst.convention || forceComment || isBasic;
|
const showConvention = !!activeCst.convention || forceComment || isBasic;
|
||||||
|
|
||||||
useEffect(() => {
|
const prevActiveCstID = useRef(activeCst.id);
|
||||||
|
const prevToggleReset = useRef(toggleReset);
|
||||||
|
const prevSchema = useRef(schema);
|
||||||
|
if (
|
||||||
|
prevActiveCstID.current !== activeCst.id ||
|
||||||
|
prevToggleReset.current !== toggleReset ||
|
||||||
|
prevSchema.current !== schema
|
||||||
|
) {
|
||||||
|
prevActiveCstID.current = activeCst.id;
|
||||||
|
prevToggleReset.current = toggleReset;
|
||||||
|
prevSchema.current = schema;
|
||||||
reset({
|
reset({
|
||||||
target: activeCst.id,
|
target: activeCst.id,
|
||||||
item_data: {
|
item_data: {
|
||||||
|
@ -97,15 +117,19 @@ export function FormConstituenta({ disabled, id, toggleReset, schema, activeCst,
|
||||||
});
|
});
|
||||||
setForceComment(false);
|
setForceComment(false);
|
||||||
setLocalParse(null);
|
setLocalParse(null);
|
||||||
}, [activeCst, schema, toggleReset, reset]);
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
const prevDirty = useRef(isDirty);
|
||||||
|
if (prevDirty.current !== isDirty) {
|
||||||
|
prevDirty.current = isDirty;
|
||||||
setIsModified(isDirty);
|
setIsModified(isDirty);
|
||||||
return () => setIsModified(false);
|
}
|
||||||
}, [isDirty, activeCst, setIsModified]);
|
|
||||||
|
|
||||||
function onSubmit(data: IUpdateConstituentaDTO) {
|
function onSubmit(data: IUpdateConstituentaDTO) {
|
||||||
return cstUpdate({ itemID: schema.id, data }).then(() => reset({ ...data }));
|
return cstUpdate({ itemID: schema.id, data }).then(() => {
|
||||||
|
setIsModified(false);
|
||||||
|
reset({ ...data });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTypeGraph(event: React.MouseEvent<Element>) {
|
function handleTypeGraph(event: React.MouseEvent<Element>) {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
import { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||||
|
|
||||||
|
import { useResetOnChange } from '@/hooks/use-reset-on-change';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { errorMsg } from '@/utils/labels';
|
import { errorMsg } from '@/utils/labels';
|
||||||
|
@ -69,6 +70,11 @@ export function EditorRSExpression({
|
||||||
|
|
||||||
const { checkConstituenta: checkInternal, isPending } = useCheckConstituenta();
|
const { checkConstituenta: checkInternal, isPending } = useCheckConstituenta();
|
||||||
|
|
||||||
|
useResetOnChange([activeCst, toggleReset], () => {
|
||||||
|
setIsModified(false);
|
||||||
|
setParseData(null);
|
||||||
|
});
|
||||||
|
|
||||||
function checkConstituenta(
|
function checkConstituenta(
|
||||||
expression: string,
|
expression: string,
|
||||||
activeCst: IConstituenta,
|
activeCst: IConstituenta,
|
||||||
|
@ -85,11 +91,6 @@ export function EditorRSExpression({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsModified(false);
|
|
||||||
setParseData(null);
|
|
||||||
}, [activeCst, toggleReset]);
|
|
||||||
|
|
||||||
function handleChange(newValue: string) {
|
function handleChange(newValue: string) {
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
setIsModified(newValue !== activeCst.definition_formal);
|
setIsModified(newValue !== activeCst.definition_formal);
|
||||||
|
|
|
@ -31,10 +31,7 @@ export function EditorRSFormCard() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div onKeyDown={handleInput} className='relative md:w-fit md:max-w-fit max-w-128 flex flex-row flex-wrap px-6 pt-8'>
|
||||||
onKeyDown={handleInput}
|
|
||||||
className='relative cc-fade-in md:w-fit md:max-w-fit max-w-128 flex flex-row flex-wrap px-6 pt-8'
|
|
||||||
>
|
|
||||||
<ToolbarItemCard
|
<ToolbarItemCard
|
||||||
className='cc-tab-tools'
|
className='cc-tab-tools'
|
||||||
onSubmit={initiateSubmit}
|
onSubmit={initiateSubmit}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|
||||||
|
@ -41,12 +41,23 @@ export function FormRSForm() {
|
||||||
reset,
|
reset,
|
||||||
formState: { isDirty, errors }
|
formState: { isDirty, errors }
|
||||||
} = useForm<IUpdateLibraryItemDTO>({
|
} = useForm<IUpdateLibraryItemDTO>({
|
||||||
resolver: zodResolver(schemaUpdateLibraryItem)
|
resolver: zodResolver(schemaUpdateLibraryItem),
|
||||||
|
defaultValues: {
|
||||||
|
id: schema.id,
|
||||||
|
item_type: LibraryItemType.RSFORM,
|
||||||
|
title: schema.title,
|
||||||
|
alias: schema.alias,
|
||||||
|
description: schema.description,
|
||||||
|
visible: schema.visible,
|
||||||
|
read_only: schema.read_only
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const visible = useWatch({ control, name: 'visible' });
|
const visible = useWatch({ control, name: 'visible' });
|
||||||
const readOnly = useWatch({ control, name: 'read_only' });
|
const readOnly = useWatch({ control, name: 'read_only' });
|
||||||
|
|
||||||
useEffect(() => {
|
const prevSchema = useRef(schema);
|
||||||
|
if (prevSchema.current !== schema) {
|
||||||
|
prevSchema.current = schema;
|
||||||
reset({
|
reset({
|
||||||
id: schema.id,
|
id: schema.id,
|
||||||
item_type: LibraryItemType.RSFORM,
|
item_type: LibraryItemType.RSFORM,
|
||||||
|
@ -56,11 +67,13 @@ export function FormRSForm() {
|
||||||
visible: schema.visible,
|
visible: schema.visible,
|
||||||
read_only: schema.read_only
|
read_only: schema.read_only
|
||||||
});
|
});
|
||||||
}, [schema, reset]);
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const prevDirty = useRef(isDirty);
|
||||||
|
if (prevDirty.current !== isDirty) {
|
||||||
|
prevDirty.current = isDirty;
|
||||||
setIsModified(isDirty);
|
setIsModified(isDirty);
|
||||||
}, [isDirty, setIsModified]);
|
}
|
||||||
|
|
||||||
function handleSelectVersion(version: CurrentVersion) {
|
function handleSelectVersion(version: CurrentVersion) {
|
||||||
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });
|
router.push({ path: urls.schema(schema.id, version === 'latest' ? undefined : version) });
|
||||||
|
|
|
@ -126,7 +126,7 @@ export function EditorRSList() {
|
||||||
const tableHeight = useFitHeight('4rem + 5px');
|
const tableHeight = useFitHeight('4rem + 5px');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div tabIndex={-1} onKeyDown={handleKeyDown} className='relative cc-fade-in pt-8'>
|
<div tabIndex={-1} onKeyDown={handleKeyDown} className='relative pt-8'>
|
||||||
{isContentEditable ? (
|
{isContentEditable ? (
|
||||||
<ToolbarRSList className='cc-tab-tools right-4 md:right-1/2 -translate-x-1/2 md:translate-x-0 cc-animate-position' />
|
<ToolbarRSList className='cc-tab-tools right-4 md:right-1/2 -translate-x-1/2 md:translate-x-0 cc-animate-position' />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
type Edge,
|
type Edge,
|
||||||
MarkerType,
|
MarkerType,
|
||||||
|
@ -121,17 +121,19 @@ export function TGFlow() {
|
||||||
}, PARAMETER.minimalTimeout);
|
}, PARAMETER.minimalTimeout);
|
||||||
}, [schema, filteredGraph, setNodes, setEdges, filter.noText, fitView, viewportInitialized, focusCst]);
|
}, [schema, filteredGraph, setNodes, setEdges, filter.noText, fitView, viewportInitialized, focusCst]);
|
||||||
|
|
||||||
useEffect(() => {
|
const prevSelected = useRef<number[]>([]);
|
||||||
if (!viewportInitialized) {
|
if (
|
||||||
return;
|
viewportInitialized &&
|
||||||
}
|
(prevSelected.current.length !== selected.length || prevSelected.current.some((id, i) => id !== selected[i]))
|
||||||
|
) {
|
||||||
|
prevSelected.current = selected;
|
||||||
setNodes(prev =>
|
setNodes(prev =>
|
||||||
prev.map(node => ({
|
prev.map(node => ({
|
||||||
...node,
|
...node,
|
||||||
selected: selected.includes(Number(node.id))
|
selected: selected.includes(Number(node.id))
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}, [selected, setNodes, viewportInitialized]);
|
}
|
||||||
|
|
||||||
function handleSetSelected(newSelection: number[]) {
|
function handleSetSelected(newSelection: number[]) {
|
||||||
setSelected(newSelection);
|
setSelected(newSelection);
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { useLibrarySearchStore } from '@/features/library';
|
import { useLibrarySearchStore } from '@/features/library';
|
||||||
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
import { useDeleteItem } from '@/features/library/backend/use-delete-item';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { useRoleStore, UserRole } from '@/features/users';
|
||||||
|
import { useAdjustRole } from '@/features/users/stores/use-adjust-role';
|
||||||
|
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
@ -39,7 +40,6 @@ export const RSEditState = ({
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
|
||||||
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
const setSearchLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
const searchLocation = useLibrarySearchStore(state => state.location);
|
const searchLocation = useLibrarySearchStore(state => state.location);
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@ export const RSEditState = ({
|
||||||
const isMutable = role > UserRole.READER && !schema.read_only;
|
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||||
const isContentEditable = isMutable && !isArchive;
|
const isContentEditable = isMutable && !isArchive;
|
||||||
const isAttachedToOSS = schema.oss.length > 0;
|
const isAttachedToOSS = schema.oss.length > 0;
|
||||||
|
const isEditor = !!user.id && schema.editors.includes(user.id);
|
||||||
|
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
|
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
|
||||||
|
@ -67,16 +68,12 @@ export const RSEditState = ({
|
||||||
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
||||||
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
|
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
|
||||||
|
|
||||||
useEffect(
|
useAdjustRole({
|
||||||
() =>
|
|
||||||
adjustRole({
|
|
||||||
isOwner: isOwned,
|
isOwner: isOwned,
|
||||||
isEditor: !!user.id && schema.editors.includes(user.id),
|
isEditor: isEditor,
|
||||||
isStaff: user.is_staff,
|
isStaff: user.is_staff,
|
||||||
adminMode: adminMode
|
adminMode: adminMode
|
||||||
}),
|
});
|
||||||
[schema, adjustRole, isOwned, user, adminMode]
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleSetFocus(newValue: IConstituenta | null) {
|
function handleSetFocus(newValue: IConstituenta | null) {
|
||||||
setFocusCst(newValue);
|
setFocusCst(newValue);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
@ -12,6 +11,7 @@ import { Divider } from '@/components/container';
|
||||||
import { TextURL } from '@/components/control';
|
import { TextURL } from '@/components/control';
|
||||||
import { type ErrorData } from '@/components/info-error';
|
import { type ErrorData } from '@/components/info-error';
|
||||||
import { useQueryStrings } from '@/hooks/use-query-strings';
|
import { useQueryStrings } from '@/hooks/use-query-strings';
|
||||||
|
import { useResetModification } from '@/hooks/use-reset-modification';
|
||||||
import { useModificationStore } from '@/stores/modification';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
import { ConstituentaTooltip } from '../../components/constituenta-tooltip';
|
import { ConstituentaTooltip } from '../../components/constituenta-tooltip';
|
||||||
|
@ -34,6 +34,8 @@ const paramsSchema = z.strictObject({
|
||||||
});
|
});
|
||||||
|
|
||||||
export function RSFormPage() {
|
export function RSFormPage() {
|
||||||
|
useResetModification();
|
||||||
|
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
|
@ -45,11 +47,9 @@ export function RSFormPage() {
|
||||||
activeID: query.get('active')
|
activeID: query.get('active')
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isModified, setIsModified } = useModificationStore();
|
const { isModified } = useModificationStore();
|
||||||
useBlockNavigation(isModified);
|
useBlockNavigation(isModified);
|
||||||
|
|
||||||
useEffect(() => setIsModified(false), [setIsModified]);
|
|
||||||
|
|
||||||
if (!urlData.id) {
|
if (!urlData.id) {
|
||||||
router.replace({ path: urls.page404, force: true });
|
router.replace({ path: urls.page404, force: true });
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
|
import { createColumnHelper, DataTable, type IConditionalStyle } from '@/components/data-table';
|
||||||
import { NoData, TextContent } from '@/components/view';
|
import { NoData, TextContent } from '@/components/view';
|
||||||
|
@ -26,11 +26,10 @@ export function TableSideConstituents({ autoScroll = true, maxHeight }: TableSid
|
||||||
const { activeCst, navigateCst } = useRSEdit();
|
const { activeCst, navigateCst } = useRSEdit();
|
||||||
const items = useFilteredItems();
|
const items = useFilteredItems();
|
||||||
|
|
||||||
useEffect(() => {
|
const prevActiveCstID = useRef<number | null>(null);
|
||||||
if (!activeCst) {
|
if (autoScroll && prevActiveCstID.current !== activeCst?.id) {
|
||||||
return;
|
prevActiveCstID.current = activeCst?.id ?? null;
|
||||||
}
|
if (!!activeCst) {
|
||||||
if (autoScroll) {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.id}`);
|
const element = document.getElementById(`${prefixes.cst_side_table}${activeCst.id}`);
|
||||||
if (element) {
|
if (element) {
|
||||||
|
@ -42,7 +41,7 @@ export function TableSideConstituents({ autoScroll = true, maxHeight }: TableSid
|
||||||
}
|
}
|
||||||
}, PARAMETER.refreshTimeout);
|
}, PARAMETER.refreshTimeout);
|
||||||
}
|
}
|
||||||
}, [activeCst, autoScroll]);
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
columnHelper.accessor('alias', {
|
columnHelper.accessor('alias', {
|
||||||
|
|
|
@ -55,7 +55,7 @@ export function FormSignup() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className='cc-column cc-fade-in mx-auto w-144 px-6 py-3'
|
className='cc-column mx-auto w-144 px-6 py-3'
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
onChange={resetErrors}
|
onChange={resetErrors}
|
||||||
>
|
>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { EditorProfile } from './editor-profile';
|
||||||
export function UserProfilePage() {
|
export function UserProfilePage() {
|
||||||
return (
|
return (
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<div className='cc-fade-in flex flex-col py-2 mx-auto w-fit'>
|
<div className='flex flex-col py-2 mx-auto w-fit'>
|
||||||
<h1 className='mb-2 select-none'>Учетные данные пользователя</h1>
|
<h1 className='mb-2 select-none'>Учетные данные пользователя</h1>
|
||||||
<div className='flex py-2'>
|
<div className='flex py-2'>
|
||||||
<EditorProfile />
|
<EditorProfile />
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
import { useRoleStore } from './role';
|
||||||
|
|
||||||
|
interface AdjustRoleProps {
|
||||||
|
isOwner: boolean;
|
||||||
|
isEditor: boolean;
|
||||||
|
isStaff: boolean;
|
||||||
|
adminMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAdjustRole(input: AdjustRoleProps) {
|
||||||
|
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||||
|
const lastInput = useRef<string | null>(null);
|
||||||
|
|
||||||
|
const serializedInput = JSON.stringify(input);
|
||||||
|
if (lastInput.current !== serializedInput) {
|
||||||
|
lastInput.current = serializedInput;
|
||||||
|
adjustRole(input);
|
||||||
|
}
|
||||||
|
}
|
13
rsconcept/frontend/src/hooks/use-reset-modification.ts
Normal file
13
rsconcept/frontend/src/hooks/use-reset-modification.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
|
export function useResetModification() {
|
||||||
|
const { setIsModified } = useModificationStore();
|
||||||
|
const initialized = useRef(false);
|
||||||
|
|
||||||
|
if (!initialized.current) {
|
||||||
|
initialized.current = true;
|
||||||
|
setIsModified(false);
|
||||||
|
}
|
||||||
|
}
|
10
rsconcept/frontend/src/hooks/use-reset-on-change.ts
Normal file
10
rsconcept/frontend/src/hooks/use-reset-on-change.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
export function useResetOnChange<T>(deps: T[], resetFn: () => void) {
|
||||||
|
const lastDeps = useRef<string | null>(null);
|
||||||
|
const currentDeps = JSON.stringify(deps);
|
||||||
|
if (lastDeps.current !== currentDeps) {
|
||||||
|
lastDeps.current = currentDeps;
|
||||||
|
resetFn();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user