fix: broken add notifications rule
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import { cn } from '@/utils/cn';
|
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
import { Link, type LinkComponentProps } from '@tanstack/react-router';
|
import { Link, type LinkComponentProps } from '@tanstack/react-router';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
@@ -6,9 +5,10 @@ import { cva } from 'class-variance-authority';
|
|||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Spinner, type SpinnerProps } from './spinner';
|
import { Spinner, type SpinnerProps } from './spinner';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:translate-y-[-0.5px] transition-all',
|
'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-all hover:translate-y-[-0.5px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -33,7 +33,7 @@ const buttonVariants = cva(
|
|||||||
variant: 'default',
|
variant: 'default',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
@@ -52,7 +52,10 @@ export interface ButtonProps
|
|||||||
function fixHeight({
|
function fixHeight({
|
||||||
autoHeight,
|
autoHeight,
|
||||||
size,
|
size,
|
||||||
}: { autoHeight?: boolean; size: ButtonProps['size'] }) {
|
}: {
|
||||||
|
autoHeight?: boolean;
|
||||||
|
size: ButtonProps['size'];
|
||||||
|
}) {
|
||||||
if (autoHeight) {
|
if (autoHeight) {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'lg':
|
case 'lg':
|
||||||
@@ -84,9 +87,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
responsive,
|
responsive,
|
||||||
autoHeight,
|
autoHeight,
|
||||||
loadingAbsolute,
|
loadingAbsolute,
|
||||||
|
type = 'button',
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref
|
||||||
) => {
|
) => {
|
||||||
const Comp = asChild ? Slot : 'button';
|
const Comp = asChild ? Slot : 'button';
|
||||||
const Icon = loading ? null : (icon ?? null);
|
const Icon = loading ? null : (icon ?? null);
|
||||||
@@ -99,31 +103,32 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant, size, className }),
|
buttonVariants({ variant, size, className }),
|
||||||
fixHeight({ autoHeight, size }),
|
fixHeight({ autoHeight, size }),
|
||||||
loadingAbsolute && 'relative',
|
loadingAbsolute && 'relative'
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
|
||||||
disabled={loading || disabled}
|
disabled={loading || disabled}
|
||||||
|
ref={ref}
|
||||||
|
type={type}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
loadingAbsolute &&
|
loadingAbsolute &&
|
||||||
'absolute top-0 left-0 right-0 bottom-0 center-center backdrop-blur bg-background/10',
|
'center-center absolute top-0 right-0 bottom-0 left-0 bg-background/10 backdrop-blur'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Spinner
|
<Spinner
|
||||||
type={loadingType}
|
|
||||||
size={spinnerSize}
|
|
||||||
speed={loadingSpeed}
|
|
||||||
variant={
|
|
||||||
variant === 'default' || variant === 'cta' ? 'white' : 'default'
|
|
||||||
}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-shrink-0',
|
'flex-shrink-0',
|
||||||
size !== 'icon' && responsive && 'mr-0 sm:mr-2',
|
size !== 'icon' && responsive && 'mr-0 sm:mr-2',
|
||||||
size !== 'icon' && !responsive && 'mr-2',
|
size !== 'icon' && !responsive && 'mr-2'
|
||||||
)}
|
)}
|
||||||
|
size={spinnerSize}
|
||||||
|
speed={loadingSpeed}
|
||||||
|
type={loadingType}
|
||||||
|
variant={
|
||||||
|
variant === 'default' || variant === 'cta' ? 'white' : 'default'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -132,7 +137,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
className={cn(
|
className={cn(
|
||||||
'h-4 w-4 flex-shrink-0',
|
'h-4 w-4 flex-shrink-0',
|
||||||
size !== 'icon' && responsive && 'mr-0 sm:mr-2',
|
size !== 'icon' && responsive && 'mr-0 sm:mr-2',
|
||||||
size !== 'icon' && !responsive && 'mr-2',
|
size !== 'icon' && !responsive && 'mr-2'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -143,7 +148,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
)}
|
)}
|
||||||
</Comp>
|
</Comp>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
Button.displayName = 'Button';
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
@@ -180,24 +185,24 @@ const LinkButton = ({
|
|||||||
<>
|
<>
|
||||||
{loading && (
|
{loading && (
|
||||||
<Spinner
|
<Spinner
|
||||||
type={loadingType}
|
|
||||||
size={spinnerSize}
|
|
||||||
speed={loadingSpeed}
|
|
||||||
variant={
|
|
||||||
variant === 'default' || variant === 'cta' ? 'white' : 'default'
|
|
||||||
}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-shrink-0',
|
'flex-shrink-0',
|
||||||
responsive && 'mr-0 sm:mr-2',
|
responsive && 'mr-0 sm:mr-2',
|
||||||
!responsive && 'mr-2',
|
!responsive && 'mr-2'
|
||||||
)}
|
)}
|
||||||
|
size={spinnerSize}
|
||||||
|
speed={loadingSpeed}
|
||||||
|
type={loadingType}
|
||||||
|
variant={
|
||||||
|
variant === 'default' || variant === 'cta' ? 'white' : 'default'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{Icon && (
|
{Icon && (
|
||||||
<Icon
|
<Icon
|
||||||
className={cn(
|
className={cn(
|
||||||
'mr-2 h-4 w-4 flex-shrink-0',
|
'mr-2 h-4 w-4 flex-shrink-0',
|
||||||
responsive && 'mr-0 sm:mr-2',
|
responsive && 'mr-0 sm:mr-2'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,28 +1,7 @@
|
|||||||
import type { RouterOutputs } from '@/trpc/client';
|
|
||||||
|
|
||||||
import { SheetContent } from '@/components/ui/sheet';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { popModal } from '.';
|
|
||||||
import { ModalHeader } from './Modal/Container';
|
|
||||||
|
|
||||||
import { ColorSquare } from '@/components/color-square';
|
|
||||||
import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label';
|
|
||||||
import { PureFilterItem } from '@/components/report/sidebar/filters/FilterItem';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
|
||||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
|
||||||
import { ComboboxEvents } from '@/components/ui/combobox-events';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { useAppParams } from '@/hooks/use-app-params';
|
|
||||||
import { useEventNames } from '@/hooks/use-event-names';
|
|
||||||
import { useEventProperties } from '@/hooks/use-event-properties';
|
|
||||||
import { useTRPC } from '@/integrations/trpc/react';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { shortId } from '@openpanel/common';
|
import { shortId } from '@openpanel/common';
|
||||||
import { zCreateNotificationRule } from '@openpanel/validation';
|
import { zCreateNotificationRule } from '@openpanel/validation';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { FilterIcon, PlusIcon, SaveIcon, TrashIcon } from 'lucide-react';
|
import { FilterIcon, PlusIcon, SaveIcon, TrashIcon } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@@ -32,7 +11,24 @@ import {
|
|||||||
useForm,
|
useForm,
|
||||||
useWatch,
|
useWatch,
|
||||||
} from 'react-hook-form';
|
} from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
import { popModal } from '.';
|
||||||
|
import { ModalHeader } from './Modal/Container';
|
||||||
|
import { ColorSquare } from '@/components/color-square';
|
||||||
|
import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label';
|
||||||
|
import { PureFilterItem } from '@/components/report/sidebar/filters/FilterItem';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
|
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||||
|
import { ComboboxEvents } from '@/components/ui/combobox-events';
|
||||||
|
import { SheetContent } from '@/components/ui/sheet';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { useAppParams } from '@/hooks/use-app-params';
|
||||||
|
import { useEventNames } from '@/hooks/use-event-names';
|
||||||
|
import { useEventProperties } from '@/hooks/use-event-properties';
|
||||||
|
import { useTRPC } from '@/integrations/trpc/react';
|
||||||
|
import type { RouterOutputs } from '@/trpc/client';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
rule?: RouterOutputs['notification']['rules'][number];
|
rule?: RouterOutputs['notification']['rules'][number];
|
||||||
@@ -71,21 +67,21 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
trpc.notification.createOrUpdateRule.mutationOptions({
|
trpc.notification.createOrUpdateRule.mutationOptions({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
toast.success(
|
toast.success(
|
||||||
rule ? 'Notification rule updated' : 'Notification rule created',
|
rule ? 'Notification rule updated' : 'Notification rule created'
|
||||||
);
|
);
|
||||||
client.refetchQueries(
|
client.refetchQueries(
|
||||||
trpc.notification.rules.queryFilter({
|
trpc.notification.rules.queryFilter({
|
||||||
projectId,
|
projectId,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
popModal();
|
popModal();
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
const integrationsQuery = useQuery(
|
const integrationsQuery = useQuery(
|
||||||
trpc.integration.list.queryOptions({
|
trpc.integration.list.queryOptions({
|
||||||
organizationId: organizationId!,
|
organizationId: organizationId!,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventsArray = useFieldArray({
|
const eventsArray = useFieldArray({
|
||||||
@@ -106,18 +102,18 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
return (
|
return (
|
||||||
<SheetContent className="[&>button.absolute]:hidden">
|
<SheetContent className="[&>button.absolute]:hidden">
|
||||||
<ModalHeader title={rule ? 'Edit rule' : 'Create rule'} />
|
<ModalHeader title={rule ? 'Edit rule' : 'Create rule'} />
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="col gap-4">
|
<form className="col gap-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<InputWithLabel
|
<InputWithLabel
|
||||||
|
error={form.formState.errors.name?.message}
|
||||||
label="Rule name"
|
label="Rule name"
|
||||||
placeholder="Eg. Sign ups on android"
|
placeholder="Eg. Sign ups on android"
|
||||||
error={form.formState.errors.name?.message}
|
|
||||||
{...form.register('name')}
|
{...form.register('name')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<WithLabel
|
<WithLabel
|
||||||
label="Type"
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
error={form.formState.errors.config?.type.message}
|
error={form.formState.errors.config?.type.message}
|
||||||
|
label="Type"
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -126,7 +122,6 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
<Combobox
|
<Combobox
|
||||||
{...field}
|
{...field}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder="Select type"
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
error={form.formState.errors.config?.type.message}
|
error={form.formState.errors.config?.type.message}
|
||||||
items={[
|
items={[
|
||||||
@@ -139,6 +134,7 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
value: 'funnel',
|
value: 'funnel',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
placeholder="Select type"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -148,16 +144,15 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
{eventsArray.fields.map((field, index) => {
|
{eventsArray.fields.map((field, index) => {
|
||||||
return (
|
return (
|
||||||
<EventField
|
<EventField
|
||||||
key={field.id}
|
|
||||||
form={form}
|
form={form}
|
||||||
index={index}
|
index={index}
|
||||||
|
key={field.id}
|
||||||
remove={() => eventsArray.remove(index)}
|
remove={() => eventsArray.remove(index)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<Button
|
<Button
|
||||||
className="self-start"
|
className="self-start"
|
||||||
variant={'outline'}
|
|
||||||
icon={PlusIcon}
|
icon={PlusIcon}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
eventsArray.append({
|
eventsArray.append({
|
||||||
@@ -166,6 +161,7 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
segment: 'event',
|
segment: 'event',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
variant={'outline'}
|
||||||
>
|
>
|
||||||
Add event
|
Add event
|
||||||
</Button>
|
</Button>
|
||||||
@@ -173,7 +169,6 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
</WithLabel>
|
</WithLabel>
|
||||||
|
|
||||||
<WithLabel
|
<WithLabel
|
||||||
label="Template"
|
|
||||||
info={
|
info={
|
||||||
<div className="prose dark:prose-invert">
|
<div className="prose dark:prose-invert">
|
||||||
<p>
|
<p>
|
||||||
@@ -197,7 +192,7 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
profile property
|
profile property
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div className="flex gap-x-2 flex-wrap">
|
<div className="flex flex-wrap gap-x-2">
|
||||||
And many more...
|
And many more...
|
||||||
<code>profileId</code>
|
<code>profileId</code>
|
||||||
<code>createdAt</code>
|
<code>createdAt</code>
|
||||||
@@ -220,6 +215,7 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
label="Template"
|
||||||
>
|
>
|
||||||
<Textarea
|
<Textarea
|
||||||
{...form.register('template')}
|
{...form.register('template')}
|
||||||
@@ -234,19 +230,19 @@ export default function AddNotificationRule({ rule }: Props) {
|
|||||||
<WithLabel label="Integrations">
|
<WithLabel label="Integrations">
|
||||||
<ComboboxAdvanced
|
<ComboboxAdvanced
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value ?? []}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
placeholder="Pick integrations"
|
|
||||||
items={integrations.map((integration) => ({
|
items={integrations.map((integration) => ({
|
||||||
label: integration.name,
|
label: integration.name,
|
||||||
value: integration.id,
|
value: integration.id,
|
||||||
}))}
|
}))}
|
||||||
|
placeholder="Pick integrations"
|
||||||
|
value={field.value ?? []}
|
||||||
/>
|
/>
|
||||||
</WithLabel>
|
</WithLabel>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" icon={SaveIcon}>
|
<Button icon={SaveIcon} type="submit">
|
||||||
{rule ? 'Update' : 'Create'}
|
{rule ? 'Update' : 'Create'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -276,27 +272,24 @@ function EventField({
|
|||||||
const properties = useEventProperties({ projectId });
|
const properties = useEventProperties({ projectId });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border bg-def-100 rounded">
|
<div className="rounded border bg-def-100">
|
||||||
<div className="row gap-2 items-center p-2">
|
<div className="row items-center gap-2 p-2">
|
||||||
<ColorSquare>{index + 1}</ColorSquare>
|
<ColorSquare>{index + 1}</ColorSquare>
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={`config.events.${index}.name`}
|
name={`config.events.${index}.name`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<ComboboxEvents
|
<ComboboxEvents
|
||||||
searchable
|
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
value={field.value}
|
|
||||||
placeholder="Select event"
|
|
||||||
onChange={field.onChange}
|
|
||||||
items={eventNames}
|
items={eventNames}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="Select event"
|
||||||
|
searchable
|
||||||
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Combobox
|
<Combobox
|
||||||
searchable
|
|
||||||
placeholder="Select a filter"
|
|
||||||
value=""
|
|
||||||
items={properties.map((item) => ({
|
items={properties.map((item) => ({
|
||||||
label: item,
|
label: item,
|
||||||
value: item,
|
value: item,
|
||||||
@@ -309,27 +302,33 @@ function EventField({
|
|||||||
value: [],
|
value: [],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
placeholder="Select a filter"
|
||||||
|
searchable
|
||||||
|
value=""
|
||||||
>
|
>
|
||||||
<Button variant={'outline'} icon={FilterIcon} size={'icon'} />
|
<Button icon={FilterIcon} size={'icon'} variant={'outline'} />
|
||||||
</Combobox>
|
</Combobox>
|
||||||
<Button
|
<Button
|
||||||
|
className="text-destructive"
|
||||||
|
icon={TrashIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
remove();
|
remove();
|
||||||
}}
|
}}
|
||||||
variant={'outline'}
|
|
||||||
className="text-destructive"
|
|
||||||
icon={TrashIcon}
|
|
||||||
size={'icon'}
|
size={'icon'}
|
||||||
|
variant={'outline'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{filtersArray.fields.map((filter, index) => {
|
{filtersArray.fields.map((filter, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={filter.id} className="p-2 border-t">
|
<div className="border-t p-2" key={filter.id}>
|
||||||
<PureFilterItem
|
<PureFilterItem
|
||||||
eventName={eventName}
|
eventName={eventName}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
onRemove={() => {
|
onChangeOperator={(operator) => {
|
||||||
filtersArray.remove(index);
|
filtersArray.update(index, {
|
||||||
|
...filter,
|
||||||
|
operator,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onChangeValue={(value) => {
|
onChangeValue={(value) => {
|
||||||
filtersArray.update(index, {
|
filtersArray.update(index, {
|
||||||
@@ -337,11 +336,8 @@ function EventField({
|
|||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onChangeOperator={(operator) => {
|
onRemove={() => {
|
||||||
filtersArray.update(index, {
|
filtersArray.remove(index);
|
||||||
...filter,
|
|
||||||
operator,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user