F: Implement react-query pt1
This commit is contained in:
parent
bfae07c7b6
commit
d899e17fcd
|
@ -46,6 +46,8 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- html-to-image
|
- html-to-image
|
||||||
- zustand
|
- zustand
|
||||||
- @tanstack/react-table
|
- @tanstack/react-table
|
||||||
|
- @tanstack/react-query
|
||||||
|
- @tanstack/react-query-devtools
|
||||||
- @uiw/react-codemirror
|
- @uiw/react-codemirror
|
||||||
- @uiw/codemirror-themes
|
- @uiw/codemirror-themes
|
||||||
- @lezer/lr
|
- @lezer/lr
|
||||||
|
|
55
rsconcept/frontend/package-lock.json
generated
55
rsconcept/frontend/package-lock.json
generated
|
@ -10,6 +10,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
|
"@tanstack/react-query": "^5.64.1",
|
||||||
|
"@tanstack/react-query-devtools": "^5.64.1",
|
||||||
"@tanstack/react-table": "^8.20.6",
|
"@tanstack/react-table": "^8.20.6",
|
||||||
"@uiw/codemirror-themes": "^4.23.7",
|
"@uiw/codemirror-themes": "^4.23.7",
|
||||||
"@uiw/react-codemirror": "^4.23.7",
|
"@uiw/react-codemirror": "^4.23.7",
|
||||||
|
@ -2994,6 +2996,59 @@
|
||||||
"@sinonjs/commons": "^3.0.0"
|
"@sinonjs/commons": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.64.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz",
|
||||||
|
"integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-devtools": {
|
||||||
|
"version": "5.62.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz",
|
||||||
|
"integrity": "sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.64.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz",
|
||||||
|
"integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.64.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query-devtools": {
|
||||||
|
"version": "5.64.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.64.1.tgz",
|
||||||
|
"integrity": "sha512-8ajcGE3wXYlb4KuJnkFYkJwJKc/qmPNTpQD7YTgLRMBPTGGp1xk7VMzxL87DoXuweO8luplUUblJJ3noVs/luQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-devtools": "5.62.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tanstack/react-query": "^5.64.1",
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/react-table": {
|
"node_modules/@tanstack/react-table": {
|
||||||
"version": "8.20.6",
|
"version": "8.20.6",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz",
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
|
"@tanstack/react-query": "^5.64.1",
|
||||||
|
"@tanstack/react-query-devtools": "^5.64.1",
|
||||||
"@tanstack/react-table": "^8.20.6",
|
"@tanstack/react-table": "^8.20.6",
|
||||||
"@uiw/codemirror-themes": "^4.23.7",
|
"@uiw/codemirror-themes": "^4.23.7",
|
||||||
"@uiw/react-codemirror": "^4.23.7",
|
"@uiw/react-codemirror": "^4.23.7",
|
||||||
|
|
|
@ -19,6 +19,9 @@ function ApplicationLayout() {
|
||||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||||
const noFooter = useAppLayoutStore(state => state.noFooter);
|
const noFooter = useAppLayoutStore(state => state.noFooter);
|
||||||
|
|
||||||
|
// TODO: prefetch data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationState>
|
<NavigationState>
|
||||||
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
|
import { queryClient } from '@/backend/queryClient';
|
||||||
import { AuthState } from '@/context/AuthContext';
|
import { AuthState } from '@/context/AuthContext';
|
||||||
import { GlobalOssState } from '@/context/GlobalOssContext';
|
import { GlobalOssState } from '@/context/GlobalOssContext';
|
||||||
import { LibraryState } from '@/context/LibraryContext';
|
import { LibraryState } from '@/context/LibraryContext';
|
||||||
import { UsersState } from '@/context/UsersContext';
|
|
||||||
|
|
||||||
import ErrorFallback from './ErrorFallback';
|
import ErrorFallback from './ErrorFallback';
|
||||||
|
|
||||||
|
@ -30,18 +32,18 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
|
||||||
onError={logError}
|
onError={logError}
|
||||||
>
|
>
|
||||||
<IntlProvider locale='ru' defaultLocale='ru'>
|
<IntlProvider locale='ru' defaultLocale='ru'>
|
||||||
<UsersState>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthState>
|
<AuthState>
|
||||||
<LibraryState>
|
<LibraryState>
|
||||||
<GlobalOssState>
|
<GlobalOssState>
|
||||||
|
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
</GlobalOssState>
|
</GlobalOssState>
|
||||||
</LibraryState>
|
</LibraryState>
|
||||||
</AuthState>
|
</AuthState>
|
||||||
</UsersState>
|
</QueryClientProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</ErrorBoundary>);
|
</ErrorBoundary>);
|
||||||
}
|
}
|
||||||
|
|
21
rsconcept/frontend/src/backend/queryClient.ts
Normal file
21
rsconcept/frontend/src/backend/queryClient.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
declare module '@tanstack/react-query' {
|
||||||
|
interface Register {
|
||||||
|
defaultError: AxiosError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
gcTime: 24 * 60 * 60 * 1000,
|
||||||
|
retry: 3,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchOnMount: true,
|
||||||
|
refetchOnReconnect: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
18
rsconcept/frontend/src/backend/users/api.ts
Normal file
18
rsconcept/frontend/src/backend/users/api.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { IUserInfo } from '@/models/user';
|
||||||
|
|
||||||
|
import { axiosInstance } from '../apiConfiguration';
|
||||||
|
|
||||||
|
export const usersApi = {
|
||||||
|
baseKey: 'users',
|
||||||
|
getUsersQueryOptions: () => {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: [usersApi.baseKey, 'list'],
|
||||||
|
queryFn: meta =>
|
||||||
|
axiosInstance.get<IUserInfo[]>(`/users/api/active-users`, {
|
||||||
|
signal: meta.signal
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
25
rsconcept/frontend/src/backend/users/useLabelUser.tsx
Normal file
25
rsconcept/frontend/src/backend/users/useLabelUser.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { useUsers } from './useUsers';
|
||||||
|
|
||||||
|
export function useLabelUser() {
|
||||||
|
const { users } = useUsers();
|
||||||
|
|
||||||
|
function getUserLabel(userID: number | null): string {
|
||||||
|
const user = users.find(({ id }) => id === userID);
|
||||||
|
if (!user || userID === null) {
|
||||||
|
return userID ? `Аноним ${userID.toString()}` : 'Отсутствует';
|
||||||
|
}
|
||||||
|
const hasFirstName = user.first_name !== '';
|
||||||
|
const hasLastName = user.last_name !== '';
|
||||||
|
if (hasFirstName || hasLastName) {
|
||||||
|
if (!hasLastName) {
|
||||||
|
return user.first_name;
|
||||||
|
}
|
||||||
|
if (!hasFirstName) {
|
||||||
|
return user.last_name;
|
||||||
|
}
|
||||||
|
return user.last_name + ' ' + user.first_name;
|
||||||
|
}
|
||||||
|
return `Аноним ${userID.toString()}`;
|
||||||
|
}
|
||||||
|
return getUserLabel;
|
||||||
|
}
|
19
rsconcept/frontend/src/backend/users/useUsers.tsx
Normal file
19
rsconcept/frontend/src/backend/users/useUsers.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { usersApi } from './api';
|
||||||
|
|
||||||
|
export function useUsersSuspense() {
|
||||||
|
const { data: users, refetch } = useSuspenseQuery({
|
||||||
|
...usersApi.getUsersQueryOptions()
|
||||||
|
});
|
||||||
|
|
||||||
|
return { users: users?.data ?? [], refetch };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUsers() {
|
||||||
|
const { data: users, refetch } = useQuery({
|
||||||
|
...usersApi.getUsersQueryOptions()
|
||||||
|
});
|
||||||
|
|
||||||
|
return { users: users?.data ?? [], refetch };
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useUsers } from '@/context/UsersContext';
|
import { useLabelUser } from '@/backend/users/useLabelUser';
|
||||||
import { UserID } from '@/models/user';
|
import { UserID } from '@/models/user';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
|
@ -12,8 +12,7 @@ interface InfoUsersProps extends CProps.Styling {
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoUsers({ items, className, prefix, header, ...restProps }: InfoUsersProps) {
|
function InfoUsers({ items, className, prefix, header, ...restProps }: InfoUsersProps) {
|
||||||
const { getUserLabel } = useUsers();
|
const getUserLabel = useLabelUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('flex flex-col dense', className)} {...restProps}>
|
<div className={clsx('flex flex-col dense', className)} {...restProps}>
|
||||||
{header ? <h2>{header}</h2> : null}
|
{header ? <h2>{header}</h2> : null}
|
||||||
|
|
|
@ -2,17 +2,18 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useUsers } from '@/context/UsersContext';
|
import { useLabelUser } from '@/backend/users/useLabelUser';
|
||||||
import { IUserInfo, UserID } from '@/models/user';
|
import { useUsers } from '@/backend/users/useUsers';
|
||||||
|
import { UserID } from '@/models/user';
|
||||||
import { matchUser } from '@/models/userAPI';
|
import { matchUser } from '@/models/userAPI';
|
||||||
|
|
||||||
import { CProps } from '../props';
|
import { CProps } from '../props';
|
||||||
import SelectSingle from '../ui/SelectSingle';
|
import SelectSingle from '../ui/SelectSingle';
|
||||||
|
|
||||||
interface SelectUserProps extends CProps.Styling {
|
interface SelectUserProps extends CProps.Styling {
|
||||||
items?: IUserInfo[];
|
|
||||||
value?: UserID;
|
value?: UserID;
|
||||||
onSelectValue: (newValue: UserID) => void;
|
onSelectValue: (newValue: UserID) => void;
|
||||||
|
filter?: (userID: UserID) => boolean;
|
||||||
|
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
|
@ -20,20 +21,23 @@ interface SelectUserProps extends CProps.Styling {
|
||||||
|
|
||||||
function SelectUser({
|
function SelectUser({
|
||||||
className,
|
className,
|
||||||
items,
|
filter,
|
||||||
value,
|
value,
|
||||||
onSelectValue,
|
onSelectValue,
|
||||||
placeholder = 'Выберите пользователя',
|
placeholder = 'Выберите пользователя',
|
||||||
...restProps
|
...restProps
|
||||||
}: SelectUserProps) {
|
}: SelectUserProps) {
|
||||||
const { getUserLabel } = useUsers();
|
const { users } = useUsers();
|
||||||
|
const getUserLabel = useLabelUser();
|
||||||
|
|
||||||
|
const items = filter ? users.filter(user => filter(user.id)) : users;
|
||||||
const options =
|
const options =
|
||||||
items?.map(user => ({
|
items?.map(user => ({
|
||||||
value: user.id,
|
value: user.id,
|
||||||
label: getUserLabel(user.id)
|
label: getUserLabel(user.id)
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
function filter(option: { value: UserID | undefined; label: string }, inputValue: string) {
|
function filterLabel(option: { value: UserID | undefined; label: string }, inputValue: string) {
|
||||||
const user = items?.find(item => item.id === option.value);
|
const user = items?.find(item => item.id === option.value);
|
||||||
return !user ? false : matchUser(user, inputValue);
|
return !user ? false : matchUser(user, inputValue);
|
||||||
}
|
}
|
||||||
|
@ -47,7 +51,7 @@ function SelectUser({
|
||||||
if (data?.value !== undefined) onSelectValue(data.value);
|
if (data?.value !== undefined) onSelectValue(data.value);
|
||||||
}}
|
}}
|
||||||
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||||
filterOption={filter}
|
filterOption={filterLabel}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,7 +19,6 @@ import {
|
||||||
IPasswordTokenData,
|
IPasswordTokenData,
|
||||||
IRequestPasswordData,
|
IRequestPasswordData,
|
||||||
IResetPasswordData,
|
IResetPasswordData,
|
||||||
IUserInfo,
|
|
||||||
IUserLoginData,
|
IUserLoginData,
|
||||||
IUserProfile,
|
IUserProfile,
|
||||||
IUserSignupData,
|
IUserSignupData,
|
||||||
|
@ -27,8 +26,6 @@ import {
|
||||||
} from '@/models/user';
|
} from '@/models/user';
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
import { contextOutsideScope } from '@/utils/labels';
|
||||||
|
|
||||||
import { useUsers } from './UsersContext';
|
|
||||||
|
|
||||||
interface IAuthContext {
|
interface IAuthContext {
|
||||||
user: ICurrentUser | undefined;
|
user: ICurrentUser | undefined;
|
||||||
login: (data: IUserLoginData, callback?: DataCallback) => void;
|
login: (data: IUserLoginData, callback?: DataCallback) => void;
|
||||||
|
@ -53,7 +50,6 @@ export const useAuth = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthState = ({ children }: React.PropsWithChildren) => {
|
export const AuthState = ({ children }: React.PropsWithChildren) => {
|
||||||
const { users } = useUsers();
|
|
||||||
const [user, setUser] = useState<ICurrentUser | undefined>(undefined);
|
const [user, setUser] = useState<ICurrentUser | undefined>(undefined);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<ErrorData>(undefined);
|
const [error, setError] = useState<ErrorData>(undefined);
|
||||||
|
@ -110,7 +106,8 @@ export const AuthState = ({ children }: React.PropsWithChildren) => {
|
||||||
onError: setError,
|
onError: setError,
|
||||||
onSuccess: newData =>
|
onSuccess: newData =>
|
||||||
reload(() => {
|
reload(() => {
|
||||||
users.push(newData as IUserInfo);
|
// TODO: reload users / optimistic update
|
||||||
|
// users.push(newData as IUserInfo);
|
||||||
callback?.(newData);
|
callback?.(newData);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,8 +8,6 @@ import { ErrorData } from '@/components/info/InfoError';
|
||||||
import { IUserProfile, IUserUpdateData } from '@/models/user';
|
import { IUserProfile, IUserUpdateData } from '@/models/user';
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
import { contextOutsideScope } from '@/utils/labels';
|
||||||
|
|
||||||
import { useUsers } from './UsersContext';
|
|
||||||
|
|
||||||
interface IUserProfileContext {
|
interface IUserProfileContext {
|
||||||
user: IUserProfile | undefined;
|
user: IUserProfile | undefined;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -31,7 +29,6 @@ export const useUserProfile = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserProfileState = ({ children }: React.PropsWithChildren) => {
|
export const UserProfileState = ({ children }: React.PropsWithChildren) => {
|
||||||
const { users } = useUsers();
|
|
||||||
const [user, setUser] = useState<IUserProfile | undefined>(undefined);
|
const [user, setUser] = useState<IUserProfile | undefined>(undefined);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
|
@ -59,16 +56,12 @@ export const UserProfileState = ({ children }: React.PropsWithChildren) => {
|
||||||
onError: setErrorProcessing,
|
onError: setErrorProcessing,
|
||||||
onSuccess: newData => {
|
onSuccess: newData => {
|
||||||
setUser(newData);
|
setUser(newData);
|
||||||
const libraryUser = users.find(item => item.id === user?.id);
|
// TODO: reload users / optimistic update
|
||||||
if (libraryUser) {
|
|
||||||
libraryUser.first_name = newData.first_name;
|
|
||||||
libraryUser.last_name = newData.last_name;
|
|
||||||
}
|
|
||||||
callback?.(newData);
|
callback?.(newData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setUser, users, user?.id]
|
[setUser]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { getActiveUsers } from '@/backend/users';
|
|
||||||
import { IUserInfo } from '@/models/user';
|
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
|
||||||
|
|
||||||
interface IUsersContext {
|
|
||||||
users: IUserInfo[];
|
|
||||||
reload: (callback?: () => void) => void;
|
|
||||||
getUserLabel: (userID: number | null) => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UsersContext = createContext<IUsersContext | null>(null);
|
|
||||||
export const useUsers = (): IUsersContext => {
|
|
||||||
const context = useContext(UsersContext);
|
|
||||||
if (context === null) {
|
|
||||||
throw new Error(contextOutsideScope('useUsers', 'UsersState'));
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UsersState = ({ children }: React.PropsWithChildren) => {
|
|
||||||
const [users, setUsers] = useState<IUserInfo[]>([]);
|
|
||||||
|
|
||||||
function getUserLabel(userID: number | null) {
|
|
||||||
const user = users.find(({ id }) => id === userID);
|
|
||||||
if (!user) {
|
|
||||||
return userID ? userID.toString() : 'Отсутствует';
|
|
||||||
}
|
|
||||||
const hasFirstName = user.first_name !== '';
|
|
||||||
const hasLastName = user.last_name !== '';
|
|
||||||
if (hasFirstName || hasLastName) {
|
|
||||||
if (!hasLastName) {
|
|
||||||
return user.first_name;
|
|
||||||
}
|
|
||||||
if (!hasFirstName) {
|
|
||||||
return user.last_name;
|
|
||||||
}
|
|
||||||
return user.last_name + ' ' + user.first_name;
|
|
||||||
}
|
|
||||||
return `Аноним ${userID}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = useCallback(
|
|
||||||
(callback?: () => void) => {
|
|
||||||
getActiveUsers({
|
|
||||||
showError: true,
|
|
||||||
onError: () => setUsers([]),
|
|
||||||
onSuccess: newData => {
|
|
||||||
newData.sort((a, b) => {
|
|
||||||
if (a.last_name === '') {
|
|
||||||
if (b.last_name === '') {
|
|
||||||
return a.id - b.id;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} else if (b.last_name === '') {
|
|
||||||
if (a.last_name === '') {
|
|
||||||
return a.id - b.id;
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
} else if (a.last_name !== b.last_name) {
|
|
||||||
return a.last_name.localeCompare(b.last_name);
|
|
||||||
} else {
|
|
||||||
return a.first_name.localeCompare(b.first_name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setUsers(newData);
|
|
||||||
callback?.();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[setUsers]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload();
|
|
||||||
}, [reload]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UsersContext
|
|
||||||
value={{
|
|
||||||
users,
|
|
||||||
reload,
|
|
||||||
getUserLabel
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</UsersContext>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -3,12 +3,12 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useUsers } from '@/backend/users/useUsers';
|
||||||
import { IconRemove } from '@/components/Icons';
|
import { IconRemove } from '@/components/Icons';
|
||||||
import SelectUser from '@/components/select/SelectUser';
|
import SelectUser from '@/components/select/SelectUser';
|
||||||
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 from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
import { useUsers } from '@/context/UsersContext';
|
|
||||||
import { UserID } from '@/models/user';
|
import { UserID } from '@/models/user';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ function DlgEditEditors() {
|
||||||
const { editors, setEditors } = useDialogsStore(state => state.props as DlgEditEditorsProps);
|
const { editors, setEditors } = useDialogsStore(state => state.props as DlgEditEditorsProps);
|
||||||
const [selected, setSelected] = useState<UserID[]>(editors);
|
const [selected, setSelected] = useState<UserID[]>(editors);
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const filtered = users.filter(user => !selected.includes(user.id));
|
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
setEditors(selected);
|
setEditors(selected);
|
||||||
|
@ -61,7 +60,12 @@ function DlgEditEditors() {
|
||||||
|
|
||||||
<div className='flex items-center gap-3'>
|
<div className='flex items-center gap-3'>
|
||||||
<Label text='Добавить' />
|
<Label text='Добавить' />
|
||||||
<SelectUser items={filtered} value={undefined} onSelectValue={onAddEditor} className='w-[25rem]' />
|
<SelectUser
|
||||||
|
filter={id => !selected.includes(id)}
|
||||||
|
value={undefined}
|
||||||
|
onSelectValue={onAddEditor}
|
||||||
|
className='w-[25rem]'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useLayoutEffect, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useLabelUser } from '@/backend/users/useLabelUser';
|
||||||
import { IconFolderTree } from '@/components/Icons';
|
import { IconFolderTree } from '@/components/Icons';
|
||||||
import BadgeLocation from '@/components/info/BadgeLocation';
|
import BadgeLocation from '@/components/info/BadgeLocation';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
|
@ -13,7 +14,6 @@ import FlexColumn from '@/components/ui/FlexColumn';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useUsers } from '@/context/UsersContext';
|
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { ILibraryItem, LibraryItemType } from '@/models/library';
|
import { ILibraryItem, LibraryItemType } from '@/models/library';
|
||||||
import { useFitHeight } from '@/stores/appLayout';
|
import { useFitHeight } from '@/stores/appLayout';
|
||||||
|
@ -30,7 +30,7 @@ const columnHelper = createColumnHelper<ILibraryItem>();
|
||||||
function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
function TableLibraryItems({ items }: TableLibraryItemsProps) {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { getUserLabel } = useUsers();
|
const getUserLabel = useLabelUser();
|
||||||
|
|
||||||
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
const folderMode = useLibrarySearchStore(state => state.folderMode);
|
||||||
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
const toggleFolderMode = useLibrarySearchStore(state => state.toggleFolderMode);
|
||||||
|
|
|
@ -19,7 +19,6 @@ import DropdownButton from '@/components/ui/DropdownButton';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import SearchBar from '@/components/ui/SearchBar';
|
import SearchBar from '@/components/ui/SearchBar';
|
||||||
import SelectorButton from '@/components/ui/SelectorButton';
|
import SelectorButton from '@/components/ui/SelectorButton';
|
||||||
import { useUsers } from '@/context/UsersContext';
|
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { LocationHead } from '@/models/library';
|
import { LocationHead } from '@/models/library';
|
||||||
import { useHasCustomFilter, useLibrarySearchStore } from '@/stores/librarySearch';
|
import { useHasCustomFilter, useLibrarySearchStore } from '@/stores/librarySearch';
|
||||||
|
@ -35,7 +34,6 @@ interface ToolbarSearchProps {
|
||||||
function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
const headMenu = useDropdown();
|
const headMenu = useDropdown();
|
||||||
const userMenu = useDropdown();
|
const userMenu = useDropdown();
|
||||||
const { users } = useUsers();
|
|
||||||
|
|
||||||
const query = useLibrarySearchStore(state => state.query);
|
const query = useLibrarySearchStore(state => state.query);
|
||||||
const setQuery = useLibrarySearchStore(state => state.setQuery);
|
const setQuery = useLibrarySearchStore(state => state.setQuery);
|
||||||
|
@ -128,7 +126,6 @@ function ToolbarSearch({ total, filtered }: ToolbarSearchProps) {
|
||||||
noBorder
|
noBorder
|
||||||
placeholder='Выберите владельца'
|
placeholder='Выберите владельца'
|
||||||
className='min-w-[15rem] text-sm mx-1 mb-1'
|
className='min-w-[15rem] text-sm mx-1 mb-1'
|
||||||
items={users}
|
|
||||||
value={filterUser}
|
value={filterUser}
|
||||||
onSelectValue={setFilterUser}
|
onSelectValue={setFilterUser}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { useCallback } from 'react';
|
import { Suspense, useCallback } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useLabelUser } from '@/backend/users/useLabelUser';
|
||||||
import {
|
import {
|
||||||
IconDateCreate,
|
IconDateCreate,
|
||||||
IconDateUpdate,
|
IconDateUpdate,
|
||||||
|
@ -13,12 +14,12 @@ import {
|
||||||
import InfoUsers from '@/components/info/InfoUsers';
|
import InfoUsers from '@/components/info/InfoUsers';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import SelectUser from '@/components/select/SelectUser';
|
import SelectUser from '@/components/select/SelectUser';
|
||||||
|
import Loader from '@/components/ui/Loader';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import Tooltip from '@/components/ui/Tooltip';
|
import Tooltip from '@/components/ui/Tooltip';
|
||||||
import ValueIcon from '@/components/ui/ValueIcon';
|
import ValueIcon from '@/components/ui/ValueIcon';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useUsers } from '@/context/UsersContext';
|
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
|
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
|
||||||
import { UserID, UserRole } from '@/models/user';
|
import { UserID, UserRole } from '@/models/user';
|
||||||
|
@ -34,7 +35,7 @@ interface EditorLibraryItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemProps) {
|
function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemProps) {
|
||||||
const { getUserLabel, users } = useUsers();
|
const getUserLabel = useLabelUser();
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
|
@ -95,7 +96,6 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
||||||
{ownerSelector.isOpen ? (
|
{ownerSelector.isOpen ? (
|
||||||
<SelectUser
|
<SelectUser
|
||||||
className='w-[25rem] sm:w-[26rem] text-sm'
|
className='w-[25rem] sm:w-[26rem] text-sm'
|
||||||
items={users}
|
|
||||||
value={item.owner ?? undefined}
|
value={item.owner ?? undefined}
|
||||||
onSelectValue={onSelectUser}
|
onSelectValue={onSelectUser}
|
||||||
/>
|
/>
|
||||||
|
@ -121,7 +121,9 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
||||||
disabled={isModified || controller.isProcessing || role < UserRole.OWNER}
|
disabled={isModified || controller.isProcessing || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
|
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
|
||||||
|
<Suspense fallback={<Loader scale={2} />}>
|
||||||
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} header='Редакторы' />
|
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} header='Редакторы' />
|
||||||
|
</Suspense>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<ValueIcon
|
<ValueIcon
|
||||||
|
|
Loading…
Reference in New Issue
Block a user