refactor(builder): extract shared ArrayItemFieldsEditor from SectionTypePanel/GenericPropsEditor
Both panels had a ~90-line near-byte-identical per-item field editor (boolean->checkbox, number->number, /color/->color, long->textarea, else text, fields derived from Object.keys(items[0])). Moved into a new ArrayItemFieldsEditor component in ArrayItemFields.tsx used by both. SectionTypePanel keeps its own FeaturesGrid special-case (key === 'features' -> FeaturesEditor) and its empty-array guard ahead of the shared call. Behavior-preserving (task E4.2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useEditor } from '@craftjs/core';
|
||||||
|
import { CollapsibleSection, ArrayPropEditor, smallInputStyle } from './shared';
|
||||||
|
|
||||||
|
/* ---------- Shared array-item field editor ----------
|
||||||
|
Extracted from SectionTypePanel and GenericPropsEditor, which both had a
|
||||||
|
near-byte-identical per-item field renderer for generic array props
|
||||||
|
(features/items/plans/testimonials/etc.). Infers an input type per field:
|
||||||
|
boolean -> checkbox, number -> number, /color/ -> color swatch, long
|
||||||
|
string -> textarea, else text. Fields are derived from Object.keys(items[0]).
|
||||||
|
Callers keep their own special-casing (e.g. SectionTypePanel routes
|
||||||
|
key === 'features' to FeaturesEditor instead of using this component). */
|
||||||
|
export const ArrayItemFieldsEditor: React.FC<{ selectedId: string; propKey: string; items: any[] }> = ({
|
||||||
|
selectedId, propKey, items,
|
||||||
|
}) => {
|
||||||
|
const { actions } = useEditor();
|
||||||
|
const arrayItems = items;
|
||||||
|
const sampleItem = arrayItems[0] || {};
|
||||||
|
const itemFields = typeof sampleItem === 'object' && sampleItem !== null ? Object.keys(sampleItem) : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleSection title={propKey.replace(/([A-Z])/g, ' $1').trim()}>
|
||||||
|
<ArrayPropEditor
|
||||||
|
selectedId={selectedId}
|
||||||
|
propKey={propKey}
|
||||||
|
items={arrayItems}
|
||||||
|
renderItem={(item: any, index: number) => {
|
||||||
|
if (typeof item !== 'object' || item === null) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={String(item)}
|
||||||
|
onChange={(e) => {
|
||||||
|
actions.setProp(selectedId, (props: any) => {
|
||||||
|
const updated = [...(props[propKey] || [])];
|
||||||
|
updated[index] = e.target.value;
|
||||||
|
props[propKey] = updated;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={smallInputStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
{itemFields.map((field) => {
|
||||||
|
const fieldVal = item[field];
|
||||||
|
if (typeof fieldVal === 'boolean') {
|
||||||
|
return (
|
||||||
|
<label key={field} style={{ fontSize: 10, color: '#71717a', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={fieldVal} onChange={(e) => {
|
||||||
|
actions.setProp(selectedId, (props: any) => {
|
||||||
|
const updated = [...(props[propKey] || [])];
|
||||||
|
updated[index] = { ...updated[index], [field]: e.target.checked };
|
||||||
|
props[propKey] = updated;
|
||||||
|
});
|
||||||
|
}} />
|
||||||
|
{field}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof fieldVal === 'number') {
|
||||||
|
return (
|
||||||
|
<div key={field}>
|
||||||
|
<label style={{ fontSize: 9, color: '#52525b', textTransform: 'capitalize' }}>{field}</label>
|
||||||
|
<input type="number" value={fieldVal} onChange={(e) => {
|
||||||
|
actions.setProp(selectedId, (props: any) => {
|
||||||
|
const updated = [...(props[propKey] || [])];
|
||||||
|
updated[index] = { ...updated[index], [field]: parseFloat(e.target.value) || 0 };
|
||||||
|
props[propKey] = updated;
|
||||||
|
});
|
||||||
|
}} style={smallInputStyle} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// color fields
|
||||||
|
if (/color/i.test(field) && typeof fieldVal === 'string') {
|
||||||
|
return (
|
||||||
|
<div key={field} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<label style={{ fontSize: 9, color: '#52525b', textTransform: 'capitalize', width: 50 }}>{field}</label>
|
||||||
|
<input type="color" value={fieldVal || '#000000'} onChange={(e) => {
|
||||||
|
actions.setProp(selectedId, (props: any) => {
|
||||||
|
const updated = [...(props[propKey] || [])];
|
||||||
|
updated[index] = { ...updated[index], [field]: e.target.value };
|
||||||
|
props[propKey] = updated;
|
||||||
|
});
|
||||||
|
}} style={{ width: 24, height: 20, border: 'none', cursor: 'pointer', background: 'none', padding: 0 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// long text
|
||||||
|
const strVal = String(fieldVal ?? '');
|
||||||
|
const isLongField = strVal.length > 50 || field === 'description' || field === 'text' || field === 'content';
|
||||||
|
return (
|
||||||
|
<div key={field}>
|
||||||
|
<label style={{ fontSize: 9, color: '#52525b', textTransform: 'capitalize' }}>{field}</label>
|
||||||
|
{isLongField ? (
|
||||||
|
<textarea value={strVal} onChange={(e) => {
|
||||||
|
actions.setProp(selectedId, (props: any) => {
|
||||||
|
const updated = [...(props[propKey] || [])];
|
||||||
|
updated[index] = { ...updated[index], [field]: e.target.value };
|
||||||
|
props[propKey] = updated;
|
||||||
|
});
|
||||||
|
}} rows={2} style={{ ...smallInputStyle, resize: 'vertical' }} />
|
||||||
|
) : (
|
||||||
|
<input type="text" value={strVal} onChange={(e) => {
|
||||||
|
actions.setProp(selectedId, (props: any) => {
|
||||||
|
const updated = [...(props[propKey] || [])];
|
||||||
|
updated[index] = { ...updated[index], [field]: e.target.value };
|
||||||
|
props[propKey] = updated;
|
||||||
|
});
|
||||||
|
}} style={smallInputStyle} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
emptyItem={typeof sampleItem === 'object' && sampleItem !== null
|
||||||
|
? Object.fromEntries(itemFields.map((f) => [f, typeof sampleItem[f] === 'number' ? 0 : typeof sampleItem[f] === 'boolean' ? false : '']))
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CollapsibleSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useEditor } from '@craftjs/core';
|
|
||||||
import {
|
import {
|
||||||
TEXT_COLORS,
|
TEXT_COLORS,
|
||||||
BG_COLORS,
|
BG_COLORS,
|
||||||
@@ -12,20 +11,17 @@ import {
|
|||||||
PresetButtonGrid,
|
PresetButtonGrid,
|
||||||
CollapsibleSection,
|
CollapsibleSection,
|
||||||
ColorPickerField,
|
ColorPickerField,
|
||||||
ArrayPropEditor,
|
|
||||||
labelStyle,
|
labelStyle,
|
||||||
inputStyle,
|
inputStyle,
|
||||||
smallInputStyle,
|
|
||||||
sectionGap,
|
sectionGap,
|
||||||
useNodeProp,
|
useNodeProp,
|
||||||
} from './shared';
|
} from './shared';
|
||||||
|
import { ArrayItemFieldsEditor } from './ArrayItemFields';
|
||||||
|
|
||||||
/* ---------- SMART GENERIC PROPS EDITOR (Fallback) ---------- */
|
/* ---------- SMART GENERIC PROPS EDITOR (Fallback) ---------- */
|
||||||
export const GenericPropsEditor: React.FC<{ selectedId: string; nodeProps: Record<string, any>; typeName: string }> = ({
|
export const GenericPropsEditor: React.FC<{ selectedId: string; nodeProps: Record<string, any>; typeName: string }> = ({
|
||||||
selectedId, nodeProps, typeName,
|
selectedId, nodeProps, typeName,
|
||||||
}) => {
|
}) => {
|
||||||
const { actions } = useEditor();
|
|
||||||
|
|
||||||
const SKIP_PROPS = new Set(['style', 'children', 'cssId', 'cssClass']);
|
const SKIP_PROPS = new Set(['style', 'children', 'cssId', 'cssClass']);
|
||||||
|
|
||||||
const { setProp: setPropValue, setPropStyle: setStyleValue } = useNodeProp(selectedId);
|
const { setProp: setPropValue, setPropStyle: setStyleValue } = useNodeProp(selectedId);
|
||||||
@@ -98,116 +94,9 @@ export const GenericPropsEditor: React.FC<{ selectedId: string; nodeProps: Recor
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Array props */}
|
{/* Array props */}
|
||||||
{arrayProps.map(([key, items]) => {
|
{arrayProps.map(([key, items]) => (
|
||||||
const arrayItems = items as any[];
|
<ArrayItemFieldsEditor key={key} selectedId={selectedId} propKey={key} items={items as any[]} />
|
||||||
const sampleItem = arrayItems[0] || {};
|
))}
|
||||||
const itemFields = typeof sampleItem === 'object' && sampleItem !== null ? Object.keys(sampleItem) : [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CollapsibleSection key={key} title={key.replace(/([A-Z])/g, ' $1').trim()}>
|
|
||||||
<ArrayPropEditor
|
|
||||||
selectedId={selectedId}
|
|
||||||
propKey={key}
|
|
||||||
items={arrayItems}
|
|
||||||
renderItem={(item: any, index: number) => {
|
|
||||||
if (typeof item !== 'object' || item === null) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={String(item)}
|
|
||||||
onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = e.target.value;
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={smallInputStyle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
||||||
{itemFields.map((field) => {
|
|
||||||
const fieldVal = item[field];
|
|
||||||
if (typeof fieldVal === 'boolean') {
|
|
||||||
return (
|
|
||||||
<label key={field} style={{ fontSize: 10, color: '#71717a', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
|
|
||||||
<input type="checkbox" checked={fieldVal} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: e.target.checked };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} />
|
|
||||||
{field}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof fieldVal === 'number') {
|
|
||||||
return (
|
|
||||||
<div key={field}>
|
|
||||||
<label style={{ fontSize: 9, color: '#52525b', textTransform: 'capitalize' }}>{field}</label>
|
|
||||||
<input type="number" value={fieldVal} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: parseFloat(e.target.value) || 0 };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} style={smallInputStyle} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (/color/i.test(field) && typeof fieldVal === 'string') {
|
|
||||||
return (
|
|
||||||
<div key={field} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
||||||
<label style={{ fontSize: 9, color: '#52525b', textTransform: 'capitalize', width: 50 }}>{field}</label>
|
|
||||||
<input type="color" value={fieldVal || '#000000'} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: e.target.value };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} style={{ width: 24, height: 20, border: 'none', cursor: 'pointer', background: 'none', padding: 0 }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const strVal = String(fieldVal ?? '');
|
|
||||||
const isLongField = strVal.length > 50 || field === 'description' || field === 'text' || field === 'content';
|
|
||||||
return (
|
|
||||||
<div key={field}>
|
|
||||||
<label style={{ fontSize: 9, color: '#52525b', textTransform: 'capitalize' }}>{field}</label>
|
|
||||||
{isLongField ? (
|
|
||||||
<textarea value={strVal} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: e.target.value };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} rows={2} style={{ ...smallInputStyle, resize: 'vertical' }} />
|
|
||||||
) : (
|
|
||||||
<input type="text" value={strVal} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: e.target.value };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} style={smallInputStyle} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
emptyItem={typeof sampleItem === 'object' && sampleItem !== null
|
|
||||||
? Object.fromEntries(itemFields.map((f) => [f, typeof sampleItem[f] === 'number' ? 0 : typeof sampleItem[f] === 'boolean' ? false : '']))
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</CollapsibleSection>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Style controls */}
|
{/* Style controls */}
|
||||||
<CollapsibleSection title="Style">
|
<CollapsibleSection title="Style">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useEditor } from '@craftjs/core';
|
|
||||||
import {
|
import {
|
||||||
BG_COLORS,
|
BG_COLORS,
|
||||||
SPACING_PRESETS,
|
SPACING_PRESETS,
|
||||||
@@ -12,18 +11,16 @@ import {
|
|||||||
PresetButtonGrid,
|
PresetButtonGrid,
|
||||||
CollapsibleSection,
|
CollapsibleSection,
|
||||||
ColorPickerField,
|
ColorPickerField,
|
||||||
ArrayPropEditor,
|
|
||||||
labelStyle,
|
labelStyle,
|
||||||
inputStyle,
|
inputStyle,
|
||||||
smallInputStyle,
|
|
||||||
sectionGap,
|
sectionGap,
|
||||||
useNodeProp,
|
useNodeProp,
|
||||||
} from './shared';
|
} from './shared';
|
||||||
import { FeaturesEditor } from './FeaturesEditor';
|
import { FeaturesEditor } from './FeaturesEditor';
|
||||||
|
import { ArrayItemFieldsEditor } from './ArrayItemFields';
|
||||||
|
|
||||||
/* ---------- SECTION-TYPE (Accordion, Tabs, Pricing, Testimonials, etc.) ---------- */
|
/* ---------- SECTION-TYPE (Accordion, Tabs, Pricing, Testimonials, etc.) ---------- */
|
||||||
export const SectionTypePanel: React.FC<StylePanelProps & { typeName: string }> = ({ selectedId, nodeProps, typeName }) => {
|
export const SectionTypePanel: React.FC<StylePanelProps & { typeName: string }> = ({ selectedId, nodeProps, typeName }) => {
|
||||||
const { actions } = useEditor();
|
|
||||||
const { setProp, setPropStyle } = useNodeProp(selectedId);
|
const { setProp, setPropStyle } = useNodeProp(selectedId);
|
||||||
|
|
||||||
const style = nodeProps.style || {};
|
const style = nodeProps.style || {};
|
||||||
@@ -100,115 +97,8 @@ export const SectionTypePanel: React.FC<StylePanelProps & { typeName: string }>
|
|||||||
}
|
}
|
||||||
const arrayItems = items as any[];
|
const arrayItems = items as any[];
|
||||||
if (arrayItems.length === 0 && typeof arrayItems[0] !== 'object') return null;
|
if (arrayItems.length === 0 && typeof arrayItems[0] !== 'object') return null;
|
||||||
const sampleItem = arrayItems[0] || {};
|
|
||||||
const itemFields = typeof sampleItem === 'object' && sampleItem !== null ? Object.keys(sampleItem) : [];
|
|
||||||
|
|
||||||
return (
|
return <ArrayItemFieldsEditor key={key} selectedId={selectedId} propKey={key} items={arrayItems} />;
|
||||||
<CollapsibleSection key={key} title={key.replace(/([A-Z])/g, ' $1').trim()}>
|
|
||||||
<ArrayPropEditor
|
|
||||||
selectedId={selectedId}
|
|
||||||
propKey={key}
|
|
||||||
items={arrayItems}
|
|
||||||
renderItem={(item: any, index: number) => {
|
|
||||||
if (typeof item !== 'object' || item === null) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={String(item)}
|
|
||||||
onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = e.target.value;
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={smallInputStyle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
||||||
{itemFields.map((field) => {
|
|
||||||
const fieldVal = item[field];
|
|
||||||
if (typeof fieldVal === 'boolean') {
|
|
||||||
return (
|
|
||||||
<label key={field} style={{ fontSize: 10, color: '#71717a', display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer' }}>
|
|
||||||
<input type="checkbox" checked={fieldVal} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: e.target.checked };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} />
|
|
||||||
{field}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof fieldVal === 'number') {
|
|
||||||
return (
|
|
||||||
<div key={field}>
|
|
||||||
<label style={{ fontSize: 9, color: '#52525b', textTransform: 'capitalize' }}>{field}</label>
|
|
||||||
<input type="number" value={fieldVal} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: parseFloat(e.target.value) || 0 };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} style={smallInputStyle} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// color fields
|
|
||||||
if (/color/i.test(field) && typeof fieldVal === 'string') {
|
|
||||||
return (
|
|
||||||
<div key={field} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
||||||
<label style={{ fontSize: 9, color: '#52525b', textTransform: 'capitalize', width: 50 }}>{field}</label>
|
|
||||||
<input type="color" value={fieldVal || '#000000'} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: e.target.value };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} style={{ width: 24, height: 20, border: 'none', cursor: 'pointer', background: 'none', padding: 0 }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// long text
|
|
||||||
const strVal = String(fieldVal ?? '');
|
|
||||||
const isLongField = strVal.length > 50 || field === 'description' || field === 'text' || field === 'content';
|
|
||||||
return (
|
|
||||||
<div key={field}>
|
|
||||||
<label style={{ fontSize: 9, color: '#52525b', textTransform: 'capitalize' }}>{field}</label>
|
|
||||||
{isLongField ? (
|
|
||||||
<textarea value={strVal} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: e.target.value };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} rows={2} style={{ ...smallInputStyle, resize: 'vertical' }} />
|
|
||||||
) : (
|
|
||||||
<input type="text" value={strVal} onChange={(e) => {
|
|
||||||
actions.setProp(selectedId, (props: any) => {
|
|
||||||
const updated = [...(props[key] || [])];
|
|
||||||
updated[index] = { ...updated[index], [field]: e.target.value };
|
|
||||||
props[key] = updated;
|
|
||||||
});
|
|
||||||
}} style={smallInputStyle} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
emptyItem={typeof sampleItem === 'object' && sampleItem !== null
|
|
||||||
? Object.fromEntries(itemFields.map((f) => [f, typeof sampleItem[f] === 'number' ? 0 : typeof sampleItem[f] === 'boolean' ? false : '']))
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</CollapsibleSection>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Style */}
|
{/* Style */}
|
||||||
|
|||||||
Reference in New Issue
Block a user