mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-02-06 09:55:47 +00:00
PR Feedback implemented
This commit is contained in:
parent
a3e405750a
commit
588802e1a5
56
lib/src/Components/Profile/ComponentErrorBoundary.tsx
Normal file
56
lib/src/Components/Profile/ComponentErrorBoundary.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Component } from 'react'
|
||||
|
||||
import type { ErrorInfo, ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
fallback?: ReactNode
|
||||
componentName?: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error?: Error
|
||||
}
|
||||
|
||||
/**
|
||||
* Error boundary for profile components.
|
||||
* Catches errors in child components and displays a fallback UI instead of crashing the entire profile.
|
||||
*/
|
||||
export class ComponentErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Error in component ${this.props.componentName ?? 'unknown'}:`,
|
||||
error,
|
||||
errorInfo.componentStack,
|
||||
)
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback
|
||||
}
|
||||
return (
|
||||
<div className='tw:p-4 tw:text-error tw:bg-error/10 tw:rounded-lg tw:my-2'>
|
||||
<p className='tw:font-semibold'>Failed to load component</p>
|
||||
{this.props.componentName && (
|
||||
<p className='tw:text-sm tw:opacity-70'>{this.props.componentName}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@ -16,14 +16,18 @@ export const ProfileTagsForm = ({ state, setState, dataField, heading, placehold
|
||||
const defaultHeading = dataField === 'offers' ? 'Offers' : 'Needs'
|
||||
const defaultPlaceholder = dataField === 'offers' ? 'enter your offers' : 'enter your needs'
|
||||
|
||||
// Validate that defaultTags is an array
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
const rawTags = state[dataField]
|
||||
const defaultTags = Array.isArray(rawTags) ? rawTags : []
|
||||
|
||||
return (
|
||||
<div className='tw:flex-1 tw:flex tw:flex-col tw:min-h-0'>
|
||||
<h3 className='tw:text-base tw:font-semibold tw:mt-4 tw:mb-2 tw:flex-none'>
|
||||
{heading ?? defaultHeading}
|
||||
</h3>
|
||||
<TagsWidget
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
defaultTags={state[dataField]}
|
||||
defaultTags={defaultTags}
|
||||
onUpdate={(tags) => {
|
||||
setState((prevState) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
|
||||
@ -18,10 +18,17 @@ export const ProfileTagsView = ({ item, dataField, heading, hideWhenEmpty = true
|
||||
|
||||
// Get the tag IDs from the item based on dataField
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
const tagRelations = item[dataField] ?? []
|
||||
const rawTagRelations = item[dataField]
|
||||
|
||||
// Resolve tag IDs to full Tag objects
|
||||
// Validate that tagRelations is an array
|
||||
const tagRelations = Array.isArray(rawTagRelations) ? rawTagRelations : []
|
||||
|
||||
// Resolve tag IDs to full Tag objects, filtering out malformed entries
|
||||
const tags: Tag[] = tagRelations.reduce((acc: Tag[], relation) => {
|
||||
// Skip if relation is missing tags_id (runtime validation for external data)
|
||||
if (typeof relation !== 'object' || !('tags_id' in relation)) {
|
||||
return acc
|
||||
}
|
||||
const tag = allTags.find((t) => t.id === relation.tags_id)
|
||||
if (tag) acc.push(tag)
|
||||
return acc
|
||||
|
||||
@ -1,36 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoForm'
|
||||
import { CrowdfundingForm } from '#components/Profile/Subcomponents/CrowdfundingForm'
|
||||
import { GalleryForm } from '#components/Profile/Subcomponents/GalleryForm'
|
||||
import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm'
|
||||
import { ProfileStartEndForm } from '#components/Profile/Subcomponents/ProfileStartEndForm'
|
||||
import { ProfileTagsForm } from '#components/Profile/Subcomponents/ProfileTagsForm'
|
||||
import { ProfileTextForm } from '#components/Profile/Subcomponents/ProfileTextForm'
|
||||
import { ComponentErrorBoundary } from '#components/Profile/ComponentErrorBoundary'
|
||||
import { formComponentMap } from '#components/Profile/componentMaps'
|
||||
|
||||
import type { FormState } from '#types/FormState'
|
||||
import type { Item } from '#types/Item'
|
||||
import type { Key } from 'react'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ComponentMap = Record<string, React.ComponentType<any>>
|
||||
|
||||
const componentMap: ComponentMap = {
|
||||
groupSubheaders: GroupSubheaderForm,
|
||||
texts: ProfileTextForm,
|
||||
contactInfos: ContactInfoForm,
|
||||
startEnd: ProfileStartEndForm,
|
||||
crowdfundings: CrowdfundingForm,
|
||||
gallery: GalleryForm,
|
||||
inviteLinks: () => null,
|
||||
relations: () => null, // Relations are not editable in form
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
tags_component: ProfileTagsForm,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
attestations_component: () => null, // Attestations are view-only
|
||||
}
|
||||
|
||||
interface TabItem {
|
||||
id: string
|
||||
title: string
|
||||
@ -95,6 +72,7 @@ export const TabsContainerForm = ({ item, state, setState, tabs, iconAsLabels =
|
||||
updateActiveTab(index)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Prevent form submission on Enter
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
updateActiveTab(index)
|
||||
@ -111,16 +89,17 @@ export const TabsContainerForm = ({ item, state, setState, tabs, iconAsLabels =
|
||||
<div className='tw:flex-1 tw:flex tw:flex-col tw:min-h-0'>
|
||||
{/* eslint-disable-next-line security/detect-object-injection */}
|
||||
{tabs[activeTab].items.map((templateItem) => {
|
||||
const TemplateComponent = componentMap[templateItem.collection]
|
||||
const TemplateComponent = formComponentMap[templateItem.collection]
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return TemplateComponent ? (
|
||||
<TemplateComponent
|
||||
key={templateItem.id}
|
||||
item={item}
|
||||
state={state}
|
||||
setState={setState}
|
||||
{...templateItem.item}
|
||||
/>
|
||||
<ComponentErrorBoundary key={templateItem.id} componentName={templateItem.collection}>
|
||||
<TemplateComponent
|
||||
item={item}
|
||||
state={state}
|
||||
setState={setState}
|
||||
{...templateItem.item}
|
||||
/>
|
||||
</ComponentErrorBoundary>
|
||||
) : (
|
||||
<div className='tw:mt-2 tw:flex-none' key={templateItem.id}>
|
||||
{templateItem.collection} form not found
|
||||
|
||||
@ -1,38 +1,12 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { AttestationsView } from '#components/Profile/Subcomponents/AttestationsView'
|
||||
import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoView'
|
||||
import { CrowdfundingView } from '#components/Profile/Subcomponents/CrowdfundingView'
|
||||
import { GalleryView } from '#components/Profile/Subcomponents/GalleryView'
|
||||
import { GroupSubHeaderView } from '#components/Profile/Subcomponents/GroupSubHeaderView'
|
||||
import { InviteLinkView } from '#components/Profile/Subcomponents/InviteLinkView'
|
||||
import { ProfileStartEndView } from '#components/Profile/Subcomponents/ProfileStartEndView'
|
||||
import { ProfileTagsView } from '#components/Profile/Subcomponents/ProfileTagsView'
|
||||
import { ProfileTextView } from '#components/Profile/Subcomponents/ProfileTextView'
|
||||
import { RelationsView } from '#components/Profile/Subcomponents/RelationsView'
|
||||
import { ComponentErrorBoundary } from '#components/Profile/ComponentErrorBoundary'
|
||||
import { viewComponentMap } from '#components/Profile/componentMaps'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { Key } from 'react'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ComponentMap = Record<string, React.ComponentType<any>>
|
||||
|
||||
const componentMap: ComponentMap = {
|
||||
groupSubheaders: GroupSubHeaderView,
|
||||
texts: ProfileTextView,
|
||||
contactInfos: ContactInfoView,
|
||||
startEnd: ProfileStartEndView,
|
||||
gallery: GalleryView,
|
||||
crowdfundings: CrowdfundingView,
|
||||
inviteLinks: InviteLinkView,
|
||||
relations: RelationsView,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
tags_component: ProfileTagsView,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
attestations_component: AttestationsView,
|
||||
}
|
||||
|
||||
interface TabItem {
|
||||
id: string
|
||||
title: string
|
||||
@ -88,6 +62,7 @@ export const TabsContainerView = ({ item, tabs = [], iconAsLabels = false }: Pro
|
||||
<div className='tw:flex tw:bg-base-200 tw:rounded-lg tw:p-1 tw:mb-4'>
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
type='button'
|
||||
key={tab.id}
|
||||
className={`tw:flex-1 tw:flex tw:items-center tw:justify-center tw:gap-2 tw:py-2 tw:px-4 tw:rounded-md tw:transition-colors tw:cursor-pointer ${activeTab === index ? 'tw:bg-primary tw:text-primary-content' : 'hover:tw:bg-base-300'}`}
|
||||
onClick={() => {
|
||||
@ -104,10 +79,12 @@ export const TabsContainerView = ({ item, tabs = [], iconAsLabels = false }: Pro
|
||||
<div className='tw:overflow-y-auto fade tw:pb-4 tw:overflow-x-hidden'>
|
||||
{/* eslint-disable-next-line security/detect-object-injection */}
|
||||
{tabs[activeTab].items.map((templateItem) => {
|
||||
const TemplateComponent = componentMap[templateItem.collection]
|
||||
const TemplateComponent = viewComponentMap[templateItem.collection]
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return TemplateComponent ? (
|
||||
<TemplateComponent key={templateItem.id} item={item} {...templateItem.item} />
|
||||
<ComponentErrorBoundary key={templateItem.id} componentName={templateItem.collection}>
|
||||
<TemplateComponent item={item} {...templateItem.item} />
|
||||
</ComponentErrorBoundary>
|
||||
) : (
|
||||
<div className='tw:mb-6' key={templateItem.id}>
|
||||
{templateItem.collection} view not found
|
||||
|
||||
@ -1,33 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoForm'
|
||||
import { CrowdfundingForm } from '#components/Profile/Subcomponents/CrowdfundingForm'
|
||||
import { GalleryForm } from '#components/Profile/Subcomponents/GalleryForm'
|
||||
import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm'
|
||||
import { ProfileStartEndForm } from '#components/Profile/Subcomponents/ProfileStartEndForm'
|
||||
import { ProfileTagsForm } from '#components/Profile/Subcomponents/ProfileTagsForm'
|
||||
import { ProfileTextForm } from '#components/Profile/Subcomponents/ProfileTextForm'
|
||||
import { TabsContainerForm } from '#components/Profile/Subcomponents/TabsContainerForm'
|
||||
import { ComponentErrorBoundary } from '#components/Profile/ComponentErrorBoundary'
|
||||
import { formComponentMap } from '#components/Profile/componentMaps'
|
||||
|
||||
import type { FormState } from '#types/FormState'
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
const componentMap = {
|
||||
groupSubheaders: GroupSubheaderForm,
|
||||
texts: ProfileTextForm,
|
||||
contactInfos: ContactInfoForm,
|
||||
startEnd: ProfileStartEndForm,
|
||||
crowdfundings: CrowdfundingForm,
|
||||
gallery: GalleryForm,
|
||||
inviteLinks: () => null,
|
||||
relations: () => null,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
tags_component: ProfileTagsForm,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
attestations_component: () => null,
|
||||
tabs: TabsContainerForm,
|
||||
}
|
||||
|
||||
export const FlexForm = ({
|
||||
item,
|
||||
state,
|
||||
@ -40,15 +16,20 @@ export const FlexForm = ({
|
||||
return (
|
||||
<div className='tw:mt-6 tw:flex tw:flex-col tw:flex-1 tw:min-h-0'>
|
||||
{item.layer?.itemType.profileTemplate.map((templateItem) => {
|
||||
const TemplateComponent = componentMap[templateItem.collection]
|
||||
const TemplateComponent = formComponentMap[templateItem.collection]
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Key may not exist in map
|
||||
return TemplateComponent ? (
|
||||
<TemplateComponent
|
||||
<ComponentErrorBoundary
|
||||
key={templateItem.id}
|
||||
state={state}
|
||||
setState={setState}
|
||||
item={item}
|
||||
{...templateItem.item}
|
||||
/>
|
||||
componentName={String(templateItem.collection)}
|
||||
>
|
||||
<TemplateComponent
|
||||
state={state}
|
||||
setState={setState}
|
||||
item={item}
|
||||
{...templateItem.item}
|
||||
/>
|
||||
</ComponentErrorBoundary>
|
||||
) : (
|
||||
<div className='tw:mt-2 tw:flex-none' key={templateItem.id}>
|
||||
{templateItem.collection} form not found
|
||||
|
||||
@ -1,45 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
|
||||
import { AttestationsView } from '#components/Profile/Subcomponents/AttestationsView'
|
||||
import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoView'
|
||||
import { CrowdfundingView } from '#components/Profile/Subcomponents/CrowdfundingView'
|
||||
import { GalleryView } from '#components/Profile/Subcomponents/GalleryView'
|
||||
import { GroupSubHeaderView } from '#components/Profile/Subcomponents/GroupSubHeaderView'
|
||||
import { InviteLinkView } from '#components/Profile/Subcomponents/InviteLinkView'
|
||||
import { ProfileStartEndView } from '#components/Profile/Subcomponents/ProfileStartEndView'
|
||||
import { ProfileTagsView } from '#components/Profile/Subcomponents/ProfileTagsView'
|
||||
import { ProfileTextView } from '#components/Profile/Subcomponents/ProfileTextView'
|
||||
import { RelationsView } from '#components/Profile/Subcomponents/RelationsView'
|
||||
import { TabsContainerView } from '#components/Profile/Subcomponents/TabsContainerView'
|
||||
import { ComponentErrorBoundary } from '#components/Profile/ComponentErrorBoundary'
|
||||
import { viewComponentMap } from '#components/Profile/componentMaps'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
import type { Key } from 'react'
|
||||
|
||||
const componentMap = {
|
||||
groupSubheaders: GroupSubHeaderView,
|
||||
texts: ProfileTextView,
|
||||
contactInfos: ContactInfoView,
|
||||
startEnd: ProfileStartEndView,
|
||||
gallery: GalleryView,
|
||||
crowdfundings: CrowdfundingView,
|
||||
inviteLinks: InviteLinkView,
|
||||
relations: RelationsView,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
tags_component: ProfileTagsView,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
attestations_component: AttestationsView,
|
||||
tabs: TabsContainerView,
|
||||
}
|
||||
|
||||
export const FlexView = ({ item }: { item: Item }) => {
|
||||
return (
|
||||
<div className='tw:h-full tw:overflow-y-auto fade tw:px-6 tw:py-4 tw:flex tw:flex-col tw:gap-4'>
|
||||
{item.layer?.itemType.profileTemplate.map(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(templateItem: { collection: string | number; id: Key | null | undefined; item: any }) => {
|
||||
const TemplateComponent = componentMap[templateItem.collection]
|
||||
(templateItem: { collection: string; id: Key | null | undefined; item: any }) => {
|
||||
const TemplateComponent = viewComponentMap[templateItem.collection]
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Key may not exist in map
|
||||
return TemplateComponent ? (
|
||||
<TemplateComponent key={templateItem.id} item={item} {...templateItem.item} />
|
||||
<ComponentErrorBoundary key={templateItem.id} componentName={templateItem.collection}>
|
||||
<TemplateComponent item={item} {...templateItem.item} />
|
||||
</ComponentErrorBoundary>
|
||||
) : (
|
||||
<div className='tw:mx-6 tw:mb-6' key={templateItem.id}>
|
||||
{templateItem.collection} view not found
|
||||
|
||||
63
lib/src/Components/Profile/componentMaps.ts
Normal file
63
lib/src/Components/Profile/componentMaps.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { AttestationsView } from '#components/Profile/Subcomponents/AttestationsView'
|
||||
import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoForm'
|
||||
import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoView'
|
||||
import { CrowdfundingForm } from '#components/Profile/Subcomponents/CrowdfundingForm'
|
||||
import { CrowdfundingView } from '#components/Profile/Subcomponents/CrowdfundingView'
|
||||
import { GalleryForm } from '#components/Profile/Subcomponents/GalleryForm'
|
||||
import { GalleryView } from '#components/Profile/Subcomponents/GalleryView'
|
||||
import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm'
|
||||
import { GroupSubHeaderView } from '#components/Profile/Subcomponents/GroupSubHeaderView'
|
||||
import { InviteLinkView } from '#components/Profile/Subcomponents/InviteLinkView'
|
||||
import { ProfileStartEndForm } from '#components/Profile/Subcomponents/ProfileStartEndForm'
|
||||
import { ProfileStartEndView } from '#components/Profile/Subcomponents/ProfileStartEndView'
|
||||
import { ProfileTagsForm } from '#components/Profile/Subcomponents/ProfileTagsForm'
|
||||
import { ProfileTagsView } from '#components/Profile/Subcomponents/ProfileTagsView'
|
||||
import { ProfileTextForm } from '#components/Profile/Subcomponents/ProfileTextForm'
|
||||
import { ProfileTextView } from '#components/Profile/Subcomponents/ProfileTextView'
|
||||
import { RelationsView } from '#components/Profile/Subcomponents/RelationsView'
|
||||
import { TabsContainerForm } from '#components/Profile/Subcomponents/TabsContainerForm'
|
||||
import { TabsContainerView } from '#components/Profile/Subcomponents/TabsContainerView'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ComponentMap = Record<string, React.ComponentType<any>>
|
||||
|
||||
/**
|
||||
* Component map for view mode (ProfileView, TabsContainerView)
|
||||
* Maps collection names from the external data schema to React components
|
||||
*/
|
||||
export const viewComponentMap: ComponentMap = {
|
||||
groupSubheaders: GroupSubHeaderView,
|
||||
texts: ProfileTextView,
|
||||
contactInfos: ContactInfoView,
|
||||
startEnd: ProfileStartEndView,
|
||||
gallery: GalleryView,
|
||||
crowdfundings: CrowdfundingView,
|
||||
inviteLinks: InviteLinkView,
|
||||
relations: RelationsView,
|
||||
tabs: TabsContainerView,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
tags_component: ProfileTagsView,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
attestations_component: AttestationsView,
|
||||
}
|
||||
|
||||
/**
|
||||
* Component map for form/edit mode (ProfileForm, TabsContainerForm)
|
||||
* Maps collection names from the external data schema to React components
|
||||
* Some components return null as they are view-only or not editable
|
||||
*/
|
||||
export const formComponentMap: ComponentMap = {
|
||||
groupSubheaders: GroupSubheaderForm,
|
||||
texts: ProfileTextForm,
|
||||
contactInfos: ContactInfoForm,
|
||||
startEnd: ProfileStartEndForm,
|
||||
crowdfundings: CrowdfundingForm,
|
||||
gallery: GalleryForm,
|
||||
inviteLinks: () => null,
|
||||
relations: () => null, // Relations are not editable in form
|
||||
tabs: TabsContainerForm,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
tags_component: ProfileTagsForm,
|
||||
// eslint-disable-next-line camelcase -- Keys match external data schema
|
||||
attestations_component: () => null, // Attestations are view-only
|
||||
}
|
||||
@ -11,6 +11,18 @@ export interface Attestation {
|
||||
to: { directus_users_id: string }[]
|
||||
}
|
||||
|
||||
export const AttestationsContext = createContext<Attestation[]>([])
|
||||
// Using undefined as default to detect missing provider
|
||||
export const AttestationsContext = createContext<Attestation[] | undefined>(undefined)
|
||||
|
||||
export const useAttestations = () => useContext(AttestationsContext)
|
||||
export const useAttestations = (): Attestation[] => {
|
||||
const context = useContext(AttestationsContext)
|
||||
if (context === undefined) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'useAttestations: AttestationsContext.Provider is missing. ' +
|
||||
'Make sure the component is wrapped in AttestationsContext.Provider.',
|
||||
)
|
||||
return []
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user