Commit 7fe72c77 authored by Andy-bubu's avatar Andy-bubu

fix: restore dialog select interactions

parent aff4fdb3
...@@ -112,19 +112,16 @@ export default function FormSelect({ ...@@ -112,19 +112,16 @@ export default function FormSelect({
isDisabled: disabled, isDisabled: disabled,
isRequired: required, isRequired: required,
selectionMode: multiple ? 'multiple' : 'single', selectionMode: multiple ? 'multiple' : 'single',
shouldCloseOnSelect: !multiple,
variant: 'secondary', variant: 'secondary',
}; };
if (multiple) { if (multiple) {
sharedProps.selectedKeys = new Set(selectedValues.map(toOptionKey)); sharedProps.value = selectedValues.map(toOptionKey);
sharedProps.onSelectionChange = (keys) => { sharedProps.onChange = (keys) => emitChange(Array.isArray(keys) ? keys : []);
const nextKeys =
keys === 'all' ? options.map((option) => option.value) : [...keys];
emitChange(nextKeys);
};
} else { } else {
sharedProps.selectedKey = toOptionKey(selectedValues[0]); sharedProps.value = toOptionKey(selectedValues[0]);
sharedProps.onSelectionChange = (key) => emitChange([key]); sharedProps.onChange = (key) => emitChange([key]);
} }
return ( return (
...@@ -141,7 +138,9 @@ export default function FormSelect({ ...@@ -141,7 +138,9 @@ export default function FormSelect({
{displayText} {displayText}
</span> </span>
</Select.Value> </Select.Value>
<ChevronDown className="rc-filter-select__indicator" /> <Select.Indicator className="rc-filter-select__indicator">
<ChevronDown />
</Select.Indicator>
</Select.Trigger> </Select.Trigger>
<Select.Popover <Select.Popover
className="rc-form-select__popover rc-filter-select__popover" className="rc-form-select__popover rc-filter-select__popover"
......
...@@ -48,6 +48,8 @@ Map component structures as follows: ...@@ -48,6 +48,8 @@ Map component structures as follows:
All enum filter dropdowns must use the shared HeroUI `Select + ListBox` adapter (`FilterSelect.jsx`). All enum fields in business forms and dialogs must use the shared HeroUI compatibility adapter (`FormSelect.jsx`). Visible native HTML/Catalyst Select popups are prohibited throughout a Xinyuan application because their operating-system surface, selected slab, theme and viewport behavior cannot meet this contract. All enum filter dropdowns must use the shared HeroUI `Select + ListBox` adapter (`FilterSelect.jsx`). All enum fields in business forms and dialogs must use the shared HeroUI compatibility adapter (`FormSelect.jsx`). Visible native HTML/Catalyst Select popups are prohibited throughout a Xinyuan application because their operating-system surface, selected slab, theme and viewport behavior cannot meet this contract.
HeroUI v3.2 Selects must use the current controlled `value` / `onChange` contract. Single selection passes one key or `null`; multiple selection passes a key array. Do not control a Select root with deprecated `selectedKey` / `selectedKeys` and `onSelectionChange`: multi-select state will split from the trigger value, and dialog edits can appear selected in the popup without updating the draft. Render the arrow through `Select.Indicator` with the shared `ChevronDown` adapter so it is always visible and receives the component's open state. Single-select popups close after selection; multi-select popups remain open until the user dismisses them.
Closed-trigger parity is insufficient. The first-pass audit must open every Select, page-size Select, remote-reference popup, multi-select, DatePicker and dialog form dropdown in the browser. Verify the visible popup surface, 12px radius, option height, selected row, checkmark, chevron, internal scroll, viewport bounds, Portal stacking and dark theme before approving the page. A page that still has a native/Catalyst/OS popup, HeroUI default blue selected option, old page-local chevron, white dark-mode island, or uninspected dropdown state is not migrated. Closed-trigger parity is insufficient. The first-pass audit must open every Select, page-size Select, remote-reference popup, multi-select, DatePicker and dialog form dropdown in the browser. Verify the visible popup surface, 12px radius, option height, selected row, checkmark, chevron, internal scroll, viewport bounds, Portal stacking and dark theme before approving the page. A page that still has a native/Catalyst/OS popup, HeroUI default blue selected option, old page-local chevron, white dark-mode island, or uninspected dropdown state is not migrated.
Remote reference and multi-select controls must not be implemented as a HeroUI `Select` root when the popup contains multiple interactive rows. Use the shared `ReferenceSearchSelect.jsx` pattern: HeroUI `Popover` controlled by a HeroUI `Button` trigger, with only the HeroUI `ListBox` and necessary lightweight actions inside the popover. Do not render a second full-width search/input field inside the dropdown; the closed trigger is the only visible field surface. Keep `selectedKeys` / `onSelectionChange` on the `ListBox`, preserve the original ID-only callback protocol, and verify `aria-expanded` changes to `true` on click before approving the field. Remote reference and multi-select controls must not be implemented as a HeroUI `Select` root when the popup contains multiple interactive rows. Use the shared `ReferenceSearchSelect.jsx` pattern: HeroUI `Popover` controlled by a HeroUI `Button` trigger, with only the HeroUI `ListBox` and necessary lightweight actions inside the popover. Do not render a second full-width search/input field inside the dropdown; the closed trigger is the only visible field surface. Keep `selectedKeys` / `onSelectionChange` on the `ListBox`, preserve the original ID-only callback protocol, and verify `aria-expanded` changes to `true` on click before approving the field.
......
...@@ -49,6 +49,7 @@ ...@@ -49,6 +49,7 @@
- 页面 Tabs 和普通命令不放装饰图标或无决策价值的数量 Badge。 - 页面 Tabs 和普通命令不放装饰图标或无决策价值的数量 Badge。
- 实时筛选不显示查询按钮。首行最多三个字段,右侧只保留重置 icon 和轻量展开控制;不显示泛化“筛选条件”标题。 - 实时筛选不显示查询按钮。首行最多三个字段,右侧只保留重置 icon 和轻量展开控制;不显示泛化“筛选条件”标题。
- 所有 Select、Reference、Date/DateTime、Upload、Checkbox 和 Textarea 走共享 HeroUI 适配器。禁止原生 `<select>`、OS 日期弹窗、Catalyst 下拉和页面私有 Popover。 - 所有 Select、Reference、Date/DateTime、Upload、Checkbox 和 Textarea 走共享 HeroUI 适配器。禁止原生 `<select>`、OS 日期弹窗、Catalyst 下拉和页面私有 Popover。
- HeroUI Select 统一使用当前 `value / onChange` 受控接口:单选值为一个 key 或 `null`,多选值为 key 数组;禁止使用旧 `selectedKey / selectedKeys / onSelectionChange` 控制 Select。Trigger 必须渲染共享 `Select.Indicator + ChevronDown` 箭头,选项变化应立即写入弹窗草稿,单选后关闭、多选时保持弹层打开。
- DateTime 必须保留真实时间编辑;Placeholder 写期望值或示例,不写裸“可选”。 - DateTime 必须保留真实时间编辑;Placeholder 写期望值或示例,不写裸“可选”。
## 7. DataGrid 与列表 ## 7. DataGrid 与列表
......
import { ChevronDown } from '@/components/AppIcons';
import { Label, ListBox, Select } from '@heroui/react'; import { Label, ListBox, Select } from '@heroui/react';
import clsx from 'clsx';
const EMPTY_ARRAY = [];
function normalizeValue(value, multiple) {
if (multiple) {
return (Array.isArray(value) ? value : value === null || value === undefined || value === '' ? [] : [value])
.map(String);
}
return value === null || value === undefined || value === '' ? null : String(value);
}
function getDisplayText(options, value, multiple, placeholder) {
const selectedValues = multiple ? value : value === null ? EMPTY_ARRAY : [value];
const labels = options
.filter((option) => selectedValues.includes(String(option.value)))
.map((option) => String(option.label ?? option.value));
if (labels.length === 0) return placeholder;
if (!multiple || labels.length <= 2) return labels.join('、');
return `${labels.slice(0, 2).join('、')} 等 ${labels.length} 项`;
}
export default function FormSelect({
ariaLabel,
className,
disabled = false,
multiple = false,
onChange,
options = EMPTY_ARRAY,
placeholder = '请选择',
value,
}) {
const normalizedValue = normalizeValue(value, multiple);
const displayText = getDisplayText(options, normalizedValue, multiple, placeholder);
const hasSelection = multiple ? normalizedValue.length > 0 : normalizedValue !== null;
export default function FormSelect({ ariaLabel, options, value, onChange, className = '' }) {
const selected = String(value ?? '');
const current = options.find((option) => String(option.value) === selected);
return ( return (
<Select aria-label={ariaLabel} className={`rc-form-select ${className}`} selectedKey={selected} onSelectionChange={(key) => onChange?.(String(key))}> <Select
<Select.Trigger><Select.Value>{current?.label || current?.value || '请选择'}</Select.Value></Select.Trigger> aria-label={ariaLabel || placeholder}
<Select.Popover placement="bottom start"> className={clsx('rc-form-select', className)}
<ListBox aria-label={ariaLabel}>{options.map((option) => <ListBox.Item id={String(option.value)} key={option.value} textValue={String(option.label || option.value)}><Label>{option.label || option.value}</Label><ListBox.ItemIndicator /></ListBox.Item>)}</ListBox> fullWidth
isDisabled={disabled}
selectionMode={multiple ? 'multiple' : 'single'}
shouldCloseOnSelect={!multiple}
value={normalizedValue}
variant="secondary"
onChange={(nextValue) => {
if (multiple) {
onChange?.((Array.isArray(nextValue) ? nextValue : []).map(String));
return;
}
onChange?.(nextValue === null || nextValue === undefined || nextValue === '' ? null : String(nextValue));
}}
>
<Select.Trigger className="rc-form-select__trigger">
<Select.Value className="rc-form-select__value">
<span className={clsx('rc-form-select__text', !hasSelection && 'is-placeholder')}>{displayText}</span>
</Select.Value>
<Select.Indicator className="rc-form-select__indicator"><ChevronDown size={16} /></Select.Indicator>
</Select.Trigger>
<Select.Popover className="rc-form-select__popover" placement="bottom start">
<ListBox aria-label={ariaLabel || placeholder} className="rc-form-select__listbox">
{options.map((option) => (
<ListBox.Item
className="rc-form-select__option"
id={String(option.value)}
isDisabled={Boolean(option.disabled)}
key={String(option.value)}
textValue={String(option.label ?? option.value)}
>
<Label className="rc-form-select__option-label">{option.label ?? option.value}</Label>
<ListBox.ItemIndicator className="rc-form-select__option-indicator" />
</ListBox.Item>
))}
</ListBox>
</Select.Popover> </Select.Popover>
</Select> </Select>
); );
......
...@@ -129,10 +129,10 @@ function WorkbenchPagination({ pagination }) { ...@@ -129,10 +129,10 @@ function WorkbenchPagination({ pagination }) {
</Pagination.Content> </Pagination.Content>
</Pagination> </Pagination>
<div className="pm-pagination-controls"> <div className="pm-pagination-controls">
<Select aria-label="每页条数" className="pm-pagination-page-size-select" isDisabled={isDisabled} selectedKey={String(pageSize)} variant="secondary" onSelectionChange={(key) => pagination.onPageSizeChange?.(Number(key))}> <Select aria-label="每页条数" className="pm-pagination-page-size-select" isDisabled={isDisabled} value={String(pageSize)} variant="secondary" onChange={(key) => pagination.onPageSizeChange?.(Number(key))}>
<Select.Trigger> <Select.Trigger>
<span className="pm-pagination-page-size-copy"><span className="pm-pagination-page-size-label">每页</span><Select.Value className="pm-pagination-page-size-value">{pageSize} 条/页</Select.Value></span> <span className="pm-pagination-page-size-copy"><span className="pm-pagination-page-size-label">每页</span><Select.Value className="pm-pagination-page-size-value">{pageSize} 条/页</Select.Value></span>
<ChevronDown className="pm-pagination-page-size-indicator" size={14} /> <Select.Indicator className="pm-pagination-page-size-indicator"><ChevronDown size={14} /></Select.Indicator>
</Select.Trigger> </Select.Trigger>
<Select.Popover className="rc-form-select__popover rc-filter-select__popover" placement="bottom start"> <Select.Popover className="rc-form-select__popover rc-filter-select__popover" placement="bottom start">
<ListBox aria-label="每页条数"> <ListBox aria-label="每页条数">
......
...@@ -207,10 +207,23 @@ button, input, textarea, select { font: inherit; letter-spacing: 0; } ...@@ -207,10 +207,23 @@ button, input, textarea, select { font: inherit; letter-spacing: 0; }
.pm-pagination-page-size-label::after { content: ':'; } .pm-pagination-page-size-label::after { content: ':'; }
.pm-pagination-page-size-select .select__indicator, .pm-pagination-page-size-indicator { display: inline-grid !important; width: 16px; height: 16px; flex: none; align-self: center; place-items: center; color: var(--rc-text-muted); font-size: 14px; line-height: 0 !important; } .pm-pagination-page-size-select .select__indicator, .pm-pagination-page-size-indicator { display: inline-grid !important; width: 16px; height: 16px; flex: none; align-self: center; place-items: center; color: var(--rc-text-muted); font-size: 14px; line-height: 0 !important; }
.pm-pagination-page-size-indicator :is(svg, .anticon), .pm-pagination-page-size-indicator svg, .pm-pagination-page-size-select .select__indicator svg { display: block !important; width: 1em !important; height: 1em !important; margin: 0 !important; color: currentColor !important; fill: currentColor !important; } .pm-pagination-page-size-indicator :is(svg, .anticon), .pm-pagination-page-size-indicator svg, .pm-pagination-page-size-select .select__indicator svg { display: block !important; width: 1em !important; height: 1em !important; margin: 0 !important; color: currentColor !important; fill: currentColor !important; }
.rc-form-select .select__trigger { align-items: center; } .rc-form-select { width: 100%; min-width: 0; }
.rc-form-select .select__value { display: flex; min-height: 20px; align-items: center; line-height: 20px; } .rc-form-select .select__trigger, .rc-form-select__trigger { display: flex !important; width: 100%; min-height: 40px; align-items: center !important; padding: 0 40px 0 12px !important; border: 1px solid var(--rc-field-border) !important; border-radius: 8px !important; color: var(--rc-text-primary); background: var(--rc-field-background) !important; box-shadow: var(--field-shadow) !important; }
.rc-form-select .select__indicator { line-height: 0; } .rc-form-select .select__trigger:hover, .rc-form-select__trigger:hover, .rc-form-select__trigger[data-hovered='true'] { border-color: var(--rc-field-border-hover) !important; }
.rc-form-select .popover, .rc-form-select [data-slot='popover'] { border-radius: 12px; background: var(--rc-bg-surface); box-shadow: var(--rc-shadow-overlay); } .rc-form-select .select__trigger:focus-visible, .rc-form-select__trigger:focus-visible, .rc-form-select__trigger[data-focus-visible='true'] { border-color: var(--rc-field-border-focus) !important; box-shadow: var(--field-shadow), 0 0 0 2px var(--rc-brand-primary-soft) !important; }
.rc-form-select .select__value, .rc-form-select__value { display: flex; min-width: 0; min-height: 20px; flex: 1; align-items: center; color: var(--rc-text-primary); font-size: 14px; line-height: 20px; }
.rc-form-select__text { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rc-form-select__text.is-placeholder { color: var(--rc-text-muted); }
.rc-form-select .select__indicator, .rc-form-select__indicator { position: absolute !important; top: 50% !important; right: 12px !important; display: inline-grid !important; width: 16px; height: 16px; place-items: center; margin: 0 !important; color: var(--rc-text-muted); line-height: 0 !important; transform: translateY(-50%); transition: color 150ms ease, transform 150ms ease; }
.rc-form-select__indicator[data-open='true'], .rc-form-select__trigger[aria-expanded='true'] .rc-form-select__indicator { transform: translateY(-50%) rotate(180deg); }
.rc-form-select__indicator .anticon, .rc-form-select__indicator svg { display: block; width: 16px; height: 16px; margin: 0; }
.rc-form-select .popover, .rc-form-select [data-slot='popover'], .rc-form-select__popover { z-index: 210; min-width: var(--trigger-width); max-height: min(320px, calc(100dvh - 32px)); overflow-y: auto; padding: 4px !important; border: 1px solid var(--rc-border-default); border-radius: 12px !important; color: var(--rc-text-secondary); background: var(--rc-bg-surface) !important; box-shadow: var(--rc-shadow-overlay) !important; }
.rc-form-select__listbox { display: grid; gap: 2px; outline: none; }
.rc-form-select__option { display: flex; min-height: 40px; cursor: pointer; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 10px; border-radius: 8px; color: var(--rc-text-secondary); font-size: 14px; line-height: 20px; }
.rc-form-select__option:hover, .rc-form-select__option[data-hovered='true'], .rc-form-select__option[data-focused='true'] { color: var(--rc-text-primary); background: var(--default); }
.rc-form-select__option[data-selected='true'] { color: var(--rc-brand-primary); background: var(--rc-brand-primary-soft); }
.rc-form-select__option-label { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rc-form-select__option-indicator { display: grid; width: 16px; height: 16px; flex: none; place-items: center; color: var(--rc-brand-primary); }
.rc-dialog-backdrop { width: 100%; height: 100%; background: var(--backdrop); -webkit-backdrop-filter: blur(var(--rc-modal-backdrop-blur)); backdrop-filter: blur(var(--rc-modal-backdrop-blur)); } .rc-dialog-backdrop { width: 100%; height: 100%; background: var(--backdrop); -webkit-backdrop-filter: blur(var(--rc-modal-backdrop-blur)); backdrop-filter: blur(var(--rc-modal-backdrop-blur)); }
.rc-dialog-panel { --rc-dialog-current-width: var(--rc-modal-width-lg); position: relative; display: flex; width: min(calc(100vw - 80px), var(--rc-dialog-current-width)) !important; max-width: calc(100vw - 80px) !important; max-height: calc(100dvh - 32px); flex-direction: column; padding: var(--rc-modal-padding); overflow: hidden; border: 0 !important; border-radius: var(--rc-modal-radius) !important; background: var(--overlay); box-shadow: var(--rc-shadow-overlay); outline: none; } .rc-dialog-panel { --rc-dialog-current-width: var(--rc-modal-width-lg); position: relative; display: flex; width: min(calc(100vw - 80px), var(--rc-dialog-current-width)) !important; max-width: calc(100vw - 80px) !important; max-height: calc(100dvh - 32px); flex-direction: column; padding: var(--rc-modal-padding); overflow: hidden; border: 0 !important; border-radius: var(--rc-modal-radius) !important; background: var(--overlay); box-shadow: var(--rc-shadow-overlay); outline: none; }
.rc-dialog-panel--xs { --rc-dialog-current-width: var(--rc-modal-width-xs); } .rc-dialog-panel--xs { --rc-dialog-current-width: var(--rc-modal-width-xs); }
......
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { Button, Checkbox, Input, Label, ListBox, Radio, RadioGroup, Select, TextArea } from '@heroui/react'; import { Button, Checkbox, Input, Radio, RadioGroup, TextArea } from '@heroui/react';
import FileUploadField from '@/components/FileUploadField'; import FileUploadField from '@/components/FileUploadField';
import FormDatePicker from '@/components/FormDatePicker'; import FormDatePicker from '@/components/FormDatePicker';
import FormSelect from '@/components/FormSelect';
import { queryUsers, uploadFile } from '@/services/DataService'; import { queryUsers, uploadFile } from '@/services/DataService';
import ReferenceSearchSelect from './ReferenceSearchSelect'; import ReferenceSearchSelect from './ReferenceSearchSelect';
import ReferenceRecordsTable from './ReferenceRecordsTable'; import ReferenceRecordsTable from './ReferenceRecordsTable';
...@@ -210,23 +211,13 @@ function ControlledFieldEditor({ value, fieldDef, disabled, onChange }) { ...@@ -210,23 +211,13 @@ function ControlledFieldEditor({ value, fieldDef, disabled, onChange }) {
if (fieldType === 'select') { if (fieldType === 'select') {
const options = getSelectOptions(fieldDef); const options = getSelectOptions(fieldDef);
return ( return (
<Select <FormSelect
aria-label={fieldDef?.label || fieldDef?.name || '请选择'} ariaLabel={fieldDef?.label || fieldDef?.name || '请选择'}
className="rc-form-select" multiple={Boolean(fieldDef?.multiple)}
selectedKey={fieldDef?.multiple ? undefined : String(value ?? '')} options={options}
selectedKeys={fieldDef?.multiple ? toArray(value).map(String) : undefined} value={value}
selectionMode={fieldDef?.multiple ? 'multiple' : 'single'} onChange={onChange}
onSelectionChange={(keys) => { />
if (fieldDef?.multiple) {
onChange(Array.from(keys === 'all' ? [] : keys || []).map(String));
} else {
onChange(keys === null || keys === undefined || keys === '' ? null : String(keys));
}
}}
>
<Select.Trigger><Select.Value>{formatFieldValue(value, fieldDef) || '请选择'}</Select.Value></Select.Trigger>
<Select.Popover placement="bottom start"><ListBox aria-label={fieldDef?.label || '请选择'}>{options.map((option) => <ListBox.Item id={String(option.value)} key={option.value} textValue={String(option.label)}><Label>{option.label}</Label><ListBox.ItemIndicator /></ListBox.Item>)}</ListBox></Select.Popover>
</Select>
); );
} }
...@@ -480,17 +471,13 @@ export default function TailwindEditableField({ ...@@ -480,17 +471,13 @@ export default function TailwindEditableField({
const options = getSelectOptions(fieldDef); const options = getSelectOptions(fieldDef);
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<Select <FormSelect
aria-label={fieldDef?.label || fieldDef?.name || '请选择'} ariaLabel={fieldDef?.label || fieldDef?.name || '请选择'}
className="rc-form-select" multiple={Boolean(fieldDef?.multiple)}
selectedKey={fieldDef?.multiple ? undefined : String(draft || '')} options={options}
selectedKeys={fieldDef?.multiple ? toArray(draft).map(String) : undefined} value={draft}
selectionMode={fieldDef?.multiple ? 'multiple' : 'single'} onChange={setDraft}
onSelectionChange={(keys) => setDraft(fieldDef?.multiple ? Array.from(keys).map(String) : String(keys))} />
>
<Select.Trigger><Select.Value>{formatFieldValue(draft, fieldDef) || '请选择'}</Select.Value></Select.Trigger>
<Select.Popover placement="bottom start"><ListBox aria-label={fieldDef?.label || '请选择'}>{options.map((option) => <ListBox.Item id={String(option.value)} key={option.value} textValue={String(option.label)}><Label>{option.label}</Label><ListBox.ItemIndicator /></ListBox.Item>)}</ListBox></Select.Popover>
</Select>
<EditorActions onClear={clearValue} onCancel={cancel} onSave={() => save()} /> <EditorActions onClear={clearValue} onCancel={cancel} onSave={() => save()} />
{error ? <div className="rc-field-error">{error}</div> : null} {error ? <div className="rc-field-error">{error}</div> : null}
</div> </div>
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
5. 关键词查找通过 `/record/query` 的 `keyword` 查询标题;清空筛选后回到第 1 页。 5. 关键词查找通过 `/record/query` 的 `keyword` 查询标题;清空筛选后回到第 1 页。
6. 列定义、列顺序、固定列和字段显示格式由 Subject View 与 Subject 字段定义共同决定。 6. 列定义、列顺序、固定列和字段显示格式由 Subject View 与 Subject 字段定义共同决定。
7. 点击列表首列打开通用详情弹窗并进入只读查看态;点击弹窗统一的“编辑”按钮或列表操作列铅笔图标进入整体编辑态,保存后返回查看态。字段权限、锁定规则和 Subject 字段类型保持不变。 7. 点击列表首列打开通用详情弹窗并进入只读查看态;点击弹窗统一的“编辑”按钮或列表操作列铅笔图标进入整体编辑态,保存后返回查看态。字段权限、锁定规则和 Subject 字段类型保持不变。
8. 编辑态的单选和多选属性使用共享 HeroUI Select;选择后立即更新当前弹窗草稿。单选完成后关闭选项层,多选允许连续选择,最终仍由弹窗底部统一“保存”提交。
异常处理: 异常处理:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment