Compare commits
5 Commits
0960f885df
...
b757c1a1da
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b757c1a1da | ||
![]() |
aac805d478 | ||
![]() |
d230295004 | ||
![]() |
5af65ecb06 | ||
![]() |
05811fe0f8 |
1004
rsconcept/frontend/package-lock.json
generated
1004
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -18,19 +18,19 @@
|
||||||
"@uiw/react-codemirror": "^4.22.2",
|
"@uiw/react-codemirror": "^4.22.2",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^10.18.0",
|
"framer-motion": "^11.0.10",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-error-boundary": "^4.0.13",
|
"react-error-boundary": "^4.0.13",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^5.2.1",
|
||||||
"react-intl": "^6.6.8",
|
"react-intl": "^6.6.8",
|
||||||
"react-loader-spinner": "^5.4.5",
|
"react-loader-spinner": "^6.1.6",
|
||||||
"react-pdf": "^9.0.0",
|
"react-pdf": "^9.0.0",
|
||||||
"react-router-dom": "^6.24.0",
|
"react-router-dom": "^6.24.0",
|
||||||
"react-select": "^5.8.0",
|
"react-select": "^5.8.0",
|
||||||
"react-tabs": "^6.0.2",
|
"react-tabs": "^6.0.2",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^10.0.5",
|
||||||
"react-tooltip": "^5.27.0",
|
"react-tooltip": "^5.27.0",
|
||||||
"reagraph": "^4.19.2",
|
"reagraph": "^4.19.2",
|
||||||
"use-debounce": "^10.0.1"
|
"use-debounce": "^10.0.1"
|
||||||
|
@ -41,21 +41,21 @@
|
||||||
"@types/node": "^20.14.9",
|
"@types/node": "^20.14.9",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^7.14.1",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-react-refresh": "^0.4.7",
|
"eslint-plugin-react-refresh": "^0.4.7",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^12.1.0",
|
||||||
"eslint-plugin-tsdoc": "^0.2.17",
|
"eslint-plugin-tsdoc": "^0.3.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"ts-jest": "^29.1.5",
|
"ts-jest": "^29.1.5",
|
||||||
"typescript": "^5.5.2",
|
"typescript": "^5.5.2",
|
||||||
"vite": "^4.5.3"
|
"vite": "^5.3.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /library
|
Disallow: /library
|
||||||
|
Disallow: /restore-password
|
||||||
|
Disallow: /signup
|
||||||
|
Disallow: /profile
|
|
@ -3,8 +3,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 { useLibrary } from '@/context/LibraryContext';
|
|
||||||
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 { ILibraryFilter } from '@/models/miscellaneous';
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ interface LibraryStateProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LibraryState = ({ children }: LibraryStateProps) => {
|
export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
const { user } = useAuth();
|
const { user, loading: userLoading } = useAuth();
|
||||||
const { adminMode } = useConceptOptions();
|
const { adminMode } = useConceptOptions();
|
||||||
|
|
||||||
const [items, setItems] = useState<ILibraryItem[]>([]);
|
const [items, setItems] = useState<ILibraryItem[]>([]);
|
||||||
|
@ -92,8 +92,8 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
if (!filter.folderMode && filter.head) {
|
if (!filter.folderMode && filter.head) {
|
||||||
result = result.filter(item => item.location.startsWith(filter.head!));
|
result = result.filter(item => item.location.startsWith(filter.head!));
|
||||||
}
|
}
|
||||||
if (filter.folderMode && filter.folder) {
|
if (filter.folderMode && filter.location) {
|
||||||
result = result.filter(item => item.location == filter.folder);
|
result = result.filter(item => item.location == filter.location);
|
||||||
}
|
}
|
||||||
if (filter.type) {
|
if (filter.type) {
|
||||||
result = result.filter(item => item.item_type === filter.type);
|
result = result.filter(item => item.item_type === filter.type);
|
||||||
|
@ -175,16 +175,18 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
||||||
const reloadTemplates = useCallback(() => {
|
const reloadTemplates = useCallback(() => {
|
||||||
setTemplates([]);
|
setTemplates([]);
|
||||||
getTemplates({
|
getTemplates({
|
||||||
setLoading: setLoading,
|
setLoading: setProcessing,
|
||||||
onError: setLoadingError,
|
onError: setProcessingError,
|
||||||
showError: true,
|
showError: true,
|
||||||
onSuccess: newData => setTemplates(newData)
|
onSuccess: newData => setTemplates(newData)
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!userLoading) {
|
||||||
reloadItems();
|
reloadItems();
|
||||||
}, [reloadItems]);
|
}
|
||||||
|
}, [reloadItems, userLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadTemplates();
|
reloadTemplates();
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { getOssDetails } from '@/app/backendAPI';
|
import { getOssDetails } from '@/app/backendAPI';
|
||||||
import { type ErrorData } from '@/components/info/InfoError';
|
import { type ErrorData } from '@/components/info/InfoError';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
|
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
|
||||||
import { OssLoader } from '@/models/OssLoader';
|
import { OssLoader } from '@/models/OssLoader';
|
||||||
|
|
||||||
function useOssDetails({ target }: { target?: string }) {
|
function useOssDetails({ target }: { target?: string }) {
|
||||||
|
const { loading: userLoading } = useAuth();
|
||||||
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
|
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<ErrorData>(undefined);
|
const [error, setError] = useState<ErrorData>(undefined);
|
||||||
|
@ -44,8 +46,10 @@ function useOssDetails({ target }: { target?: string }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!userLoading) {
|
||||||
reload();
|
reload();
|
||||||
}, [reload]);
|
}
|
||||||
|
}, [reload, userLoading]);
|
||||||
|
|
||||||
return { schema, setSchema, reload, error, setError, loading };
|
return { schema, setSchema, reload, error, setError, loading };
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,12 @@ import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { getRSFormDetails } from '@/app/backendAPI';
|
import { getRSFormDetails } from '@/app/backendAPI';
|
||||||
import { type ErrorData } from '@/components/info/InfoError';
|
import { type ErrorData } from '@/components/info/InfoError';
|
||||||
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { IRSForm, IRSFormData } from '@/models/rsform';
|
import { IRSForm, IRSFormData } from '@/models/rsform';
|
||||||
import { RSFormLoader } from '@/models/RSFormLoader';
|
import { RSFormLoader } from '@/models/RSFormLoader';
|
||||||
|
|
||||||
function useRSFormDetails({ target, version }: { target?: string; version?: string }) {
|
function useRSFormDetails({ target, version }: { target?: string; version?: string }) {
|
||||||
|
const { loading: userLoading } = useAuth();
|
||||||
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
|
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<ErrorData>(undefined);
|
const [error, setError] = useState<ErrorData>(undefined);
|
||||||
|
@ -44,8 +46,10 @@ function useRSFormDetails({ target, version }: { target?: string; version?: stri
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!userLoading) {
|
||||||
reload();
|
reload();
|
||||||
}, [reload]);
|
}
|
||||||
|
}, [reload, userLoading]);
|
||||||
|
|
||||||
return { schema, setSchema, reload, error, setError, loading };
|
return { schema, setSchema, reload, error, setError, loading };
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,11 +144,10 @@ export interface ILibraryFilter {
|
||||||
type?: LibraryItemType;
|
type?: LibraryItemType;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
|
||||||
|
folderMode?: boolean;
|
||||||
path?: string;
|
path?: string;
|
||||||
head?: LocationHead;
|
head?: LocationHead;
|
||||||
|
location?: string;
|
||||||
folderMode?: boolean;
|
|
||||||
folder?: string;
|
|
||||||
|
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isOwned?: boolean;
|
isOwned?: boolean;
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { toggleTristateFlag } from '@/utils/utils';
|
||||||
|
|
||||||
import TableLibraryItems from './TableLibraryItems';
|
import TableLibraryItems from './TableLibraryItems';
|
||||||
import ToolbarSearch from './ToolbarSearch';
|
import ToolbarSearch from './ToolbarSearch';
|
||||||
import ViewSideFolders from './ViewSideFolders';
|
import ViewSideLocation from './ViewSideLocation';
|
||||||
|
|
||||||
function LibraryPage() {
|
function LibraryPage() {
|
||||||
const library = useLibrary();
|
const library = useLibrary();
|
||||||
|
@ -26,7 +26,7 @@ function LibraryPage() {
|
||||||
|
|
||||||
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
|
const [head, setHead] = useLocalStorage<LocationHead | undefined>(storage.librarySearchHead, undefined);
|
||||||
const [folderMode, setFolderMode] = useLocalStorage<boolean>(storage.librarySearchFolderMode, true);
|
const [folderMode, setFolderMode] = useLocalStorage<boolean>(storage.librarySearchFolderMode, true);
|
||||||
const [folder, setFolder] = useLocalStorage<string>(storage.librarySearchFolder, '');
|
const [location, setLocation] = useLocalStorage<string>(storage.librarySearchLocation, '');
|
||||||
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
|
const [isVisible, setIsVisible] = useLocalStorage<boolean | undefined>(storage.librarySearchVisible, true);
|
||||||
const [isSubscribed, setIsSubscribed] = useLocalStorage<boolean | undefined>(
|
const [isSubscribed, setIsSubscribed] = useLocalStorage<boolean | undefined>(
|
||||||
storage.librarySearchSubscribed,
|
storage.librarySearchSubscribed,
|
||||||
|
@ -45,9 +45,9 @@ function LibraryPage() {
|
||||||
isSubscribed: user ? isSubscribed : undefined,
|
isSubscribed: user ? isSubscribed : undefined,
|
||||||
isVisible: user ? isVisible : true,
|
isVisible: user ? isVisible : true,
|
||||||
folderMode: folderMode,
|
folderMode: folderMode,
|
||||||
folder: folder
|
location: location
|
||||||
}),
|
}),
|
||||||
[head, path, query, isEditor, isOwned, isSubscribed, isVisible, user, folderMode, folder]
|
[head, path, query, isEditor, isOwned, isSubscribed, isVisible, user, folderMode, location]
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasCustomFilter = useMemo(
|
const hasCustomFilter = useMemo(
|
||||||
|
@ -59,13 +59,13 @@ function LibraryPage() {
|
||||||
filter.isOwned !== undefined ||
|
filter.isOwned !== undefined ||
|
||||||
filter.isSubscribed !== undefined ||
|
filter.isSubscribed !== undefined ||
|
||||||
filter.isVisible !== true ||
|
filter.isVisible !== true ||
|
||||||
!!filter.folder,
|
!!filter.location,
|
||||||
[filter]
|
[filter]
|
||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setItems(library.applyFilter(filter));
|
setItems(library.applyFilter(filter));
|
||||||
}, [library, filter, filter.query]);
|
}, [library, library.items.length, filter]);
|
||||||
|
|
||||||
const toggleVisible = useCallback(() => setIsVisible(prev => toggleTristateFlag(prev)), [setIsVisible]);
|
const toggleVisible = useCallback(() => setIsVisible(prev => toggleTristateFlag(prev)), [setIsVisible]);
|
||||||
const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]);
|
const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]);
|
||||||
|
@ -81,13 +81,13 @@ function LibraryPage() {
|
||||||
setIsSubscribed(undefined);
|
setIsSubscribed(undefined);
|
||||||
setIsOwned(undefined);
|
setIsOwned(undefined);
|
||||||
setIsEditor(undefined);
|
setIsEditor(undefined);
|
||||||
setFolder('');
|
setLocation('');
|
||||||
}, [setHead, setIsVisible, setIsSubscribed, setIsOwned, setIsEditor, setFolder]);
|
}, [setHead, setIsVisible, setIsSubscribed, setIsOwned, setIsEditor, setLocation]);
|
||||||
|
|
||||||
const view = useMemo(
|
const viewLibrary = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<TableLibraryItems
|
<TableLibraryItems
|
||||||
resetQuery={resetFilter} // prettier: split lines
|
resetQuery={resetFilter}
|
||||||
items={items}
|
items={items}
|
||||||
folderMode={folderMode}
|
folderMode={folderMode}
|
||||||
toggleFolderMode={toggleFolderMode}
|
toggleFolderMode={toggleFolderMode}
|
||||||
|
@ -96,6 +96,18 @@ function LibraryPage() {
|
||||||
[resetFilter, items, folderMode, toggleFolderMode]
|
[resetFilter, items, folderMode, toggleFolderMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const viewLocations = useMemo(
|
||||||
|
() => (
|
||||||
|
<ViewSideLocation
|
||||||
|
active={location}
|
||||||
|
setActive={setLocation}
|
||||||
|
folderTree={library.folders}
|
||||||
|
toggleFolderMode={toggleFolderMode}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[location, library.folders, setLocation, toggleFolderMode]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataLoader
|
<DataLoader
|
||||||
id='library-page' // prettier: split lines
|
id='library-page' // prettier: split lines
|
||||||
|
@ -127,17 +139,8 @@ function LibraryPage() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>{folderMode ? viewLocations : null}</AnimatePresence>
|
||||||
{folderMode ? (
|
{viewLibrary}
|
||||||
<ViewSideFolders
|
|
||||||
currentFolder={folder} // prettier: split-lines
|
|
||||||
setFolder={setFolder}
|
|
||||||
folders={library.folders}
|
|
||||||
toggleFolderMode={toggleFolderMode}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</AnimatePresence>
|
|
||||||
{view}
|
|
||||||
</div>
|
</div>
|
||||||
</DataLoader>
|
</DataLoader>
|
||||||
);
|
);
|
||||||
|
|
|
@ -161,7 +161,7 @@ function TableLibraryItems({ items, resetQuery, folderMode, toggleFolderMode }:
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
headPosition='0'
|
headPosition='0'
|
||||||
className={clsx('text-xs sm:text-sm cc-scroll-y', { 'border-l border-b': folderMode })}
|
className={clsx('text-xs sm:text-sm cc-scroll-y h-fit', { 'border-l border-b': folderMode })}
|
||||||
style={{ maxHeight: tableHeight }}
|
style={{ maxHeight: tableHeight }}
|
||||||
noDataComponent={
|
noDataComponent={
|
||||||
<FlexColumn className='dense p-3 items-center min-h-[6rem]'>
|
<FlexColumn className='dense p-3 items-center min-h-[6rem]'>
|
||||||
|
|
|
@ -14,14 +14,14 @@ import { animateSideView } from '@/styling/animations';
|
||||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
interface ViewSideFoldersProps {
|
interface ViewSideLocationProps {
|
||||||
folders: FolderTree;
|
folderTree: FolderTree;
|
||||||
currentFolder: string;
|
active: string;
|
||||||
setFolder: React.Dispatch<React.SetStateAction<string>>;
|
setActive: React.Dispatch<React.SetStateAction<string>>;
|
||||||
toggleFolderMode: () => void;
|
toggleFolderMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewSideFolders({ folders, currentFolder, setFolder, toggleFolderMode }: ViewSideFoldersProps) {
|
function ViewSideLocation({ folderTree, active, setActive: setActive, toggleFolderMode }: ViewSideLocationProps) {
|
||||||
const handleClickFolder = useCallback(
|
const handleClickFolder = useCallback(
|
||||||
(event: CProps.EventMouse, target: FolderNode) => {
|
(event: CProps.EventMouse, target: FolderNode) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -32,10 +32,10 @@ function ViewSideFolders({ folders, currentFolder, setFolder, toggleFolderMode }
|
||||||
.then(() => toast.success(information.pathReady))
|
.then(() => toast.success(information.pathReady))
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
} else {
|
} else {
|
||||||
setFolder(target.getPath());
|
setActive(target.getPath());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setFolder]
|
[setActive]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -59,8 +59,8 @@ function ViewSideFolders({ folders, currentFolder, setFolder, toggleFolderMode }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectLocation
|
<SelectLocation
|
||||||
value={currentFolder}
|
value={active}
|
||||||
folderTree={folders}
|
folderTree={folderTree}
|
||||||
prefix={prefixes.folders_list}
|
prefix={prefixes.folders_list}
|
||||||
onClick={handleClickFolder}
|
onClick={handleClickFolder}
|
||||||
/>
|
/>
|
||||||
|
@ -68,4 +68,4 @@ function ViewSideFolders({ folders, currentFolder, setFolder, toggleFolderMode }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ViewSideFolders;
|
export default ViewSideLocation;
|
|
@ -95,7 +95,7 @@ export const storage = {
|
||||||
|
|
||||||
librarySearchHead: 'library.search.head',
|
librarySearchHead: 'library.search.head',
|
||||||
librarySearchFolderMode: 'library.search.folder_mode',
|
librarySearchFolderMode: 'library.search.folder_mode',
|
||||||
librarySearchFolder: 'library.search.folder',
|
librarySearchLocation: 'library.search.location',
|
||||||
librarySearchVisible: 'library.search.visible',
|
librarySearchVisible: 'library.search.visible',
|
||||||
librarySearchOwned: 'library.search.owned',
|
librarySearchOwned: 'library.search.owned',
|
||||||
librarySearchSubscribed: 'library.search.subscribed',
|
librarySearchSubscribed: 'library.search.subscribed',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user