mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
link and unlink profiles
This commit is contained in:
parent
62a9208f85
commit
f817e1baf7
@ -22,7 +22,7 @@ export default function AddButton({ triggerAction }: { triggerAction: React.Disp
|
||||
canAddItems() ?
|
||||
<div className="tw-dropdown tw-dropdown-top tw-dropdown-end tw-dropdown-hover tw-z-500 tw-absolute tw-right-4 tw-bottom-4" >
|
||||
<label tabIndex={0} className="tw-z-500 tw-btn tw-btn-circle tw-shadow tw-bg-base-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" className="tw-w-5 tw-h-5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="3" stroke="currentColor" className="tw-w-5 tw-h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</label>
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import * as React from "react"
|
||||
import { useRemoveItem } from "../../hooks/useItems";
|
||||
import { Item, ItemsApi } from "../../../../types";
|
||||
import { toast } from "react-toastify";
|
||||
import { useHasUserPermission } from "../../hooks/usePermissions";
|
||||
import { getValue } from "../../../../Utils/GetValue";
|
||||
import { useAssetApi } from '../../../AppShell/hooks/useAssets'
|
||||
import DialogModal from "../../../Templates/DialogModal";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { ItemFormPopupProps } from './ItemFormPopup'
|
||||
import { HeaderView } from './ItemPopupComponents/HeaderView'
|
||||
import { TextView } from './ItemPopupComponents/TextView'
|
||||
import { timeAgo } from '../../../Utils/TimeAgo'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { LatLng } from 'leaflet'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useRemoveItem } from '../hooks/useItems'
|
||||
@ -19,8 +19,9 @@ export interface ItemViewPopupProps {
|
||||
}
|
||||
|
||||
|
||||
export const ItemViewPopup = React.forwardRef((props: ItemViewPopupProps, ref: any) => {
|
||||
|
||||
|
||||
export const ItemViewPopup = React.forwardRef((props: ItemViewPopupProps, ref: any) => {
|
||||
const map = useMap();
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const removeItem = useRemoveItem();
|
||||
@ -60,7 +61,7 @@ export const ItemViewPopup = React.forwardRef((props: ItemViewPopupProps, ref: a
|
||||
return (
|
||||
<LeafletPopup ref={ref} maxHeight={377} minWidth={275} maxWidth={275} autoPanPadding={[20, 80]}>
|
||||
<div className='tw-bg-base-100 tw-text-base-content'>
|
||||
<HeaderView api={props.item.api} item={props.item} editCallback={handleEdit} deleteCallback={handleDelete} />
|
||||
<HeaderView api={props.item.layer?.api} item={props.item} editCallback={handleEdit} deleteCallback={handleDelete} />
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
{props.children ?
|
||||
|
||||
|
||||
50
src/Components/Profile/ActionsButton.tsx
Normal file
50
src/Components/Profile/ActionsButton.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useHasUserPermission, usePermissions } from "../Map/hooks/usePermissions";
|
||||
import DialogModal from "../Templates/DialogModal";
|
||||
import { useItems } from "../Map/hooks/useItems";
|
||||
import { TextView } from "../Map";
|
||||
import { HeaderView } from "../Map/Subcomponents/ItemPopupComponents/HeaderView";
|
||||
import { Item } from "../../types";
|
||||
|
||||
export function ActionButton({ item, triggerAddButton, triggerItemSelected, existingRelations, itemType, color, collection = "items" }: {
|
||||
triggerAddButton: any,
|
||||
triggerItemSelected: any,
|
||||
existingRelations: Item[],
|
||||
itemType:string;
|
||||
color: string,
|
||||
collection?: string,
|
||||
item: Item
|
||||
}) {
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
|
||||
const items = useItems();
|
||||
|
||||
const filterdItems = items.filter(i => i.type == itemType).filter(i => !existingRelations.some(s => s.id == i.id)).filter(i => i.id != item.id)
|
||||
|
||||
|
||||
return (
|
||||
<>{hasUserPermission(collection, "create") &&
|
||||
<>
|
||||
<div className="tw-absolute tw-right-4 tw-bottom-4 tw-flex tw-flex-col" >
|
||||
<button tabIndex={0} className="tw-z-500 tw-btn tw-btn-circle tw-shadow" onClick={() => { setModalOpen(true) }} style={{ backgroundColor: color, color: "#fff" }}>
|
||||
<svg className="tw-h-5 tw-w-5" stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"></path></svg>
|
||||
|
||||
</button>
|
||||
<button tabIndex={0} className="tw-z-500 tw-btn tw-btn-circle tw-shadow tw-mt-2" onClick={() => { triggerAddButton() }} style={{ backgroundColor: color, color: "#fff" }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="4" stroke="currentColor" className="tw-w-5 tw-h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<DialogModal title={"Select"} isOpened={modalOpen} onClose={() => (setModalOpen(false))}>
|
||||
{filterdItems.map(i => <div key={i.id} className='tw-cursor-pointer tw-card tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-bg-base-100 tw-text-base-content tw-mx-4 tw-p-4 tw-mb-4 tw-h-fit' onClick={()=>{triggerItemSelected(i.id);setModalOpen(false)}}>
|
||||
<HeaderView item={i} hideMenu></HeaderView>
|
||||
</div>)}
|
||||
</DialogModal>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
66
src/Components/Profile/LinkedItemsHeaderView.tsx
Normal file
66
src/Components/Profile/LinkedItemsHeaderView.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import * as React from "react"
|
||||
import { getValue } from "../../Utils/GetValue";
|
||||
import { Item, ItemsApi } from "../../types";
|
||||
import { useAssetApi } from "../AppShell/hooks/useAssets";
|
||||
import { useHasUserPermission } from "../Map/hooks/usePermissions";
|
||||
|
||||
|
||||
|
||||
|
||||
export function LinkedItemsHeaderView({ item, unlinkCallback, itemNameField, itemAvatarField, loading }: {
|
||||
item: Item,
|
||||
unlinkCallback?: any,
|
||||
itemNameField?: string,
|
||||
itemAvatarField?: string,
|
||||
loading?: boolean,
|
||||
}) {
|
||||
|
||||
const assetsApi = useAssetApi();
|
||||
|
||||
|
||||
const avatar = itemAvatarField && getValue(item, itemAvatarField) ? assetsApi.url + getValue(item, itemAvatarField) : item.layer?.itemAvatarField && item && getValue(item, item.layer?.itemAvatarField) && assetsApi.url + getValue(item, item.layer?.itemAvatarField);
|
||||
const title = itemNameField ? getValue(item, itemNameField) : item.layer?.itemNameField && item && getValue(item, item.layer?.itemNameField);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='tw-grid tw-grid-cols-6 tw-pb-2'>
|
||||
<div className='tw-col-span-5'>
|
||||
<div className="tw-flex tw-flex-row">{
|
||||
avatar ?
|
||||
<div className="tw-w-10 tw-min-w-[2.5em] tw-rounded-full">
|
||||
<img className="tw-rounded-full" src={`${avatar}?width=80&height=80`} />
|
||||
</div>
|
||||
:
|
||||
""
|
||||
}
|
||||
<b className={`tw-text-xl tw-font-bold ${avatar ? "tw-ml-2 tw-mt-1" : ""}`}>{title ? title : item.name}</b>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='tw-col-span-1' onClick={(e) => e.stopPropagation()}>
|
||||
{
|
||||
<div className="tw-dropdown tw-dropdown-bottom">
|
||||
<label tabIndex={0} className="tw-bg-base-100 tw-btn tw-m-1 tw-leading-3 tw-border-none tw-min-h-0 tw-h-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="tw-h-5 tw-w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||||
</svg>
|
||||
</label>
|
||||
<ul tabIndex={0} className="tw-dropdown-content tw-menu tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-z-1000">
|
||||
{true && <li>
|
||||
<a className='tw-cursor-pointer !tw-text-error' onClick={() => unlinkCallback(item.id)}>
|
||||
{loading ? <span className="tw-loading tw-loading-spinner tw-loading-sm"></span>
|
||||
:
|
||||
<svg className="tw-h-5 tw-w-5" stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M304.083 405.907c4.686 4.686 4.686 12.284 0 16.971l-44.674 44.674c-59.263 59.262-155.693 59.266-214.961 0-59.264-59.265-59.264-155.696 0-214.96l44.675-44.675c4.686-4.686 12.284-4.686 16.971 0l39.598 39.598c4.686 4.686 4.686 12.284 0 16.971l-44.675 44.674c-28.072 28.073-28.072 73.75 0 101.823 28.072 28.072 73.75 28.073 101.824 0l44.674-44.674c4.686-4.686 12.284-4.686 16.971 0l39.597 39.598zm-56.568-260.216c4.686 4.686 12.284 4.686 16.971 0l44.674-44.674c28.072-28.075 73.75-28.073 101.824 0 28.072 28.073 28.072 73.75 0 101.823l-44.675 44.674c-4.686 4.686-4.686 12.284 0 16.971l39.598 39.598c4.686 4.686 12.284 4.686 16.971 0l44.675-44.675c59.265-59.265 59.265-155.695 0-214.96-59.266-59.264-155.695-59.264-214.961 0l-44.674 44.674c-4.686 4.686-4.686 12.284 0 16.971l39.597 39.598zm234.828 359.28l22.627-22.627c9.373-9.373 9.373-24.569 0-33.941L63.598 7.029c-9.373-9.373-24.569-9.373-33.941 0L7.029 29.657c-9.373 9.373-9.373 24.569 0 33.941l441.373 441.373c9.373 9.372 24.569 9.372 33.941 0z"></path></svg>}
|
||||
</a>
|
||||
</li>}
|
||||
</ul>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,9 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import { MapOverlayPage, TitleCard } from '../Templates'
|
||||
import { useAddItem, useItems, useRemoveItem, useUpdateItem } from '../Map/hooks/useItems'
|
||||
import { MapOverlayPage } from '../Templates'
|
||||
import { useAddItem, useItems, useUpdateItem } from '../Map/hooks/useItems'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Item} from '../../types';
|
||||
import { Item } from '../../types';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { useMap } from 'react-leaflet';
|
||||
import { LatLng } from 'leaflet';
|
||||
@ -11,15 +10,15 @@ import { PopupStartEndInput, StartEndView, TextView } from '../Map';
|
||||
import useWindowDimensions from '../Map/hooks/useWindowDimension';
|
||||
import { useAddTag, useTags } from '../Map/hooks/useTags';
|
||||
import { useResetFilterTags } from '../Map/hooks/useFilter';
|
||||
import { HeaderView } from '../Map/Subcomponents/ItemPopupComponents/HeaderView';
|
||||
import { useHasUserPermission } from '../Map/hooks/usePermissions';
|
||||
import {PlusButton} from './PlusButton';
|
||||
import { TextAreaInput, TextInput } from '../Input';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useAuth } from '../Auth';
|
||||
import { useLayers } from '../Map/hooks/useLayers';
|
||||
import { ActionButton } from './ActionsButton';
|
||||
import { LinkedItemsHeaderView } from './LinkedItemsHeaderView';
|
||||
|
||||
export function OverlayItemProfile() {
|
||||
|
||||
@ -60,23 +59,22 @@ export function OverlayItemProfile() {
|
||||
function scroll() {
|
||||
tabRef.current?.scrollIntoView();
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
scroll();
|
||||
scroll();
|
||||
}, [addItemPopupType])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(addItemPopupType);
|
||||
|
||||
console.log(addItemPopupType);
|
||||
|
||||
}, [addItemPopupType])
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const itemId = location.pathname.split("/")[2];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
item && setItem(item);
|
||||
hasUserPermission("items", "update", item) && setAddButton(true);
|
||||
const bounds = map.getBounds();
|
||||
const x = bounds.getEast() - bounds.getWest()
|
||||
if (windowDimension.width > 768)
|
||||
@ -88,16 +86,20 @@ export function OverlayItemProfile() {
|
||||
setActiveTab(1);
|
||||
}, [location])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setRelations([]);
|
||||
item.relations?.map(r => {
|
||||
const item = items.find(i => i.id == r.related_items_id)
|
||||
item && setRelations(current => [...current, item])
|
||||
})
|
||||
|
||||
}, [item, items])
|
||||
|
||||
useEffect(() => {
|
||||
item && item.user_created && hasUserPermission("items", "update", item) && setAddButton(true);
|
||||
}, [item])
|
||||
|
||||
|
||||
const submitNewItem = async (evt: any, type: string) => {
|
||||
evt.preventDefault();
|
||||
const formItem: Item = {} as Item;
|
||||
@ -114,11 +116,11 @@ export function OverlayItemProfile() {
|
||||
});
|
||||
const uuid = crypto.randomUUID();
|
||||
console.log(layers);
|
||||
|
||||
const layer = layers.find(l => l.name.toLocaleLowerCase().replace("s","") == addItemPopupType.toLocaleLowerCase())
|
||||
|
||||
const layer = layers.find(l => l.name.toLocaleLowerCase().replace("s", "") == addItemPopupType.toLocaleLowerCase())
|
||||
|
||||
console.log(layer);
|
||||
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await layer?.api?.createItem!({ ...formItem, id: uuid, type: type });
|
||||
@ -137,13 +139,21 @@ export function OverlayItemProfile() {
|
||||
}
|
||||
|
||||
const linkItem = async (id: string) => {
|
||||
let new_relations = item.relations|| [] ;
|
||||
let new_relations = item.relations || [];
|
||||
new_relations?.push({ items_id: item.id, related_items_id: id })
|
||||
const updatedItem = { id: item.id, relations: new_relations }
|
||||
await item?.layer?.api?.updateItem!(updatedItem)
|
||||
updateItem({ ...item, relations: new_relations })
|
||||
}
|
||||
|
||||
const unlinkItem = async (id: string) => {
|
||||
console.log(id);
|
||||
|
||||
let new_relations = item.relations?.filter(r => r.related_items_id !== id)
|
||||
console.log(new_relations);
|
||||
|
||||
const updatedItem = { id: item.id, relations: new_relations }
|
||||
|
||||
await item?.layer?.api?.updateItem!(updatedItem)
|
||||
|
||||
updateItem({ ...item, relations: new_relations })
|
||||
}
|
||||
|
||||
@ -183,7 +193,7 @@ export function OverlayItemProfile() {
|
||||
if (i.type == 'project') return (
|
||||
|
||||
<div key={i.id} className='tw-cursor-pointer tw-card tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-bg-base-100 tw-text-base-content tw-mx-4 tw-p-4 tw-mb-4 tw-h-fit' onClick={() => navigate('/item/' + i.id)}>
|
||||
<HeaderView loading={loading} item={i} api={i.layer?.api} editCallback={() => navigate("/edit-item/"+i.id)}></HeaderView>
|
||||
<LinkedItemsHeaderView loading={loading} item={i} unlinkCallback={unlinkItem} />
|
||||
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
<TextView truncate item={i} />
|
||||
@ -197,7 +207,7 @@ export function OverlayItemProfile() {
|
||||
<form ref={tabRef} autoComplete='off' onSubmit={e => submitNewItem(e, addItemPopupType)} >
|
||||
|
||||
<div className='tw-cursor-pointer tw-card tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-bg-base-100 tw-text-base-content tw-mx-4 tw-p-6 tw-mb-4'>
|
||||
<label className="tw-btn tw-btn-sm tw-rounded-2xl tw-btn-circle tw-btn-ghost hover:tw-bg-transparent tw-absolute tw-right-0 tw-top-0 tw-text-gray-600" onClick={() => {
|
||||
<label className="tw-btn tw-btn-sm tw-rounded-2xl tw-btn-circle tw-btn-ghost hover:tw-bg-transparent tw-absolute tw-right-0 tw-top-0 tw-text-gray-600" onClick={() => {
|
||||
setAddItemPopupType("")
|
||||
}}>
|
||||
<p className='tw-text-center '>✕</p></label>
|
||||
@ -209,7 +219,7 @@ export function OverlayItemProfile() {
|
||||
</div>
|
||||
</form> : <></>
|
||||
}
|
||||
{ addButton && <PlusButton triggerAction={() => { setAddItemPopupType("project"); scroll() }} color={item.color}></PlusButton>}
|
||||
{addButton && <ActionButton item={item} existingRelations={relations} itemType={"project"} triggerItemSelected={linkItem} triggerAddButton={() => { setAddItemPopupType("project"); scroll() }} color={item.color}></ActionButton>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -223,7 +233,7 @@ export function OverlayItemProfile() {
|
||||
if (i.type == 'event') return (
|
||||
|
||||
<div key={i.id} className='tw-cursor-pointer tw-card tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-bg-base-100 tw-text-base-content tw-mx-4 tw-p-6 tw-mb-4' onClick={() => navigate('/item/' + i.id)}>
|
||||
<HeaderView item={i} hideMenu />
|
||||
<LinkedItemsHeaderView item={i} unlinkCallback={unlinkItem} loading={loading} />
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
<StartEndView item={i}></StartEndView>
|
||||
<TextView truncate item={i} />
|
||||
@ -250,7 +260,7 @@ export function OverlayItemProfile() {
|
||||
</div>
|
||||
</form> : <></>
|
||||
}
|
||||
{ addButton && <PlusButton triggerAction={() => { setAddItemPopupType("event"); scroll() }} color={item.color}></PlusButton>}
|
||||
{addButton && <ActionButton item={item} existingRelations={relations} itemType={"event"} triggerItemSelected={linkItem} triggerAddButton={() => { setAddItemPopupType("event"); scroll() }} color={item.color}></ActionButton>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -259,10 +269,6 @@ export function OverlayItemProfile() {
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-280px)] tw-overflow-y-auto fade tw-pt-2 tw-pb-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { useHasUserPermission, usePermissions } from "../Map/hooks/usePermissions";
|
||||
import { useAuth } from "../Auth";
|
||||
import { useHasUserPermission } from "../Map/hooks/usePermissions";
|
||||
|
||||
export function PlusButton({ triggerAction, color, collection="items" }: { triggerAction: any, color: string, collection?:string }) {
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user