update Field.tsx, TypeDropdown.tsx, Typeahead.tsx and typeahead-opt.tsx
This commit is contained in:
parent
624df96b1e
commit
4460c2bfcb
|
|
@ -14,6 +14,7 @@ export const Field: React.FC<{
|
|||
fm: any;
|
||||
label: string;
|
||||
name: string;
|
||||
isBetter?: boolean;
|
||||
onLoad?: () => Promise<any> | any;
|
||||
type?:
|
||||
| "rating"
|
||||
|
|
@ -40,7 +41,6 @@ export const Field: React.FC<{
|
|||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
hidden_label?: boolean;
|
||||
|
||||
onChange?: ({ data }: any) => Promise<void> | void;
|
||||
className?: string;
|
||||
classField?: string;
|
||||
|
|
@ -48,9 +48,11 @@ export const Field: React.FC<{
|
|||
prefix?: string | any | (() => any);
|
||||
suffix?: string | any | (() => any);
|
||||
allowNew?: boolean;
|
||||
unique?: boolean;
|
||||
}> = ({
|
||||
fm,
|
||||
label,
|
||||
isBetter,
|
||||
name,
|
||||
onLoad,
|
||||
type = "text",
|
||||
|
|
@ -65,6 +67,7 @@ export const Field: React.FC<{
|
|||
prefix,
|
||||
suffix,
|
||||
allowNew,
|
||||
unique = true,
|
||||
}) => {
|
||||
let result = null;
|
||||
const field = useLocal({
|
||||
|
|
@ -247,6 +250,8 @@ export const Field: React.FC<{
|
|||
disabled={is_disable}
|
||||
onChange={onChange}
|
||||
mode="multi"
|
||||
unique={unique}
|
||||
isBetter={isBetter}
|
||||
/>
|
||||
</>
|
||||
) : ["checkbox"].includes(type) ? (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ export const TypeDropdown: React.FC<any> = ({
|
|||
disabled,
|
||||
mode,
|
||||
allowNew = false,
|
||||
unique = true,
|
||||
isBetter = false,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -22,7 +24,7 @@ export const TypeDropdown: React.FC<any> = ({
|
|||
: []
|
||||
}
|
||||
allowNew={allowNew}
|
||||
unique={mode === "multi" ? true : false}
|
||||
unique={mode === "multi" ? (isBetter ? false : true) : false}
|
||||
disabledSearch={false}
|
||||
// popupClassName={}
|
||||
required={required}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export const Typeahead: FC<{
|
|||
note?: string;
|
||||
disabledSearch?: boolean;
|
||||
onInit?: (e: any) => void;
|
||||
isBetter?: boolean;
|
||||
}> = ({
|
||||
value,
|
||||
fitur,
|
||||
|
|
@ -59,7 +60,9 @@ export const Typeahead: FC<{
|
|||
popupClassName,
|
||||
disabledSearch,
|
||||
onInit,
|
||||
isBetter = false,
|
||||
}) => {
|
||||
const maxLength = 4;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [debouncedTerm, setDebouncedTerm] = useState("");
|
||||
const local = useLocal({
|
||||
|
|
@ -68,6 +71,10 @@ export const Typeahead: FC<{
|
|||
options: [] as OptItem[],
|
||||
loaded: false,
|
||||
loading: false,
|
||||
selectBetter: {
|
||||
all: false,
|
||||
partial: [] as any[],
|
||||
},
|
||||
search: {
|
||||
input: "",
|
||||
timeout: null as any,
|
||||
|
|
@ -102,9 +109,10 @@ export const Typeahead: FC<{
|
|||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!select_found) {
|
||||
local.select = options[0];
|
||||
if (Array.isArray(value) && value?.length) {
|
||||
if (!select_found) {
|
||||
local.select = options[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -513,6 +521,25 @@ export const Typeahead: FC<{
|
|||
resetSearch();
|
||||
}
|
||||
}}
|
||||
onRemove={(data) => {
|
||||
local.value = local.value.filter((val) => data?.value !== val);
|
||||
local.render();
|
||||
input.current?.focus();
|
||||
|
||||
if (typeof onChange === "function") {
|
||||
onChange(local.value);
|
||||
}
|
||||
}}
|
||||
onSelectAll={(data: boolean) => {
|
||||
local.value = data ? options.map((e) => e?.value) : [];
|
||||
local.render();
|
||||
input.current?.focus();
|
||||
if (typeof onChange === "function") {
|
||||
onChange(local.value);
|
||||
}
|
||||
}}
|
||||
init={local}
|
||||
isBetter={isBetter}
|
||||
loading={local.loading}
|
||||
showEmpty={!allow_new}
|
||||
className={popupClassName}
|
||||
|
|
@ -520,8 +547,89 @@ export const Typeahead: FC<{
|
|||
options={options}
|
||||
searching={local.search.searching}
|
||||
searchText={local.search.input}
|
||||
onSearch={async (e) => {
|
||||
const val = e.currentTarget.value;
|
||||
if (!local.open) {
|
||||
local.open = true;
|
||||
}
|
||||
|
||||
local.search.input = val;
|
||||
local.render();
|
||||
|
||||
if (local.search.promise) {
|
||||
await local.search.promise;
|
||||
}
|
||||
|
||||
local.search.searching = true;
|
||||
local.render();
|
||||
if (allow_new) {
|
||||
setSearchTerm(val);
|
||||
}
|
||||
if (local.search.searching) {
|
||||
if (local.local_search) {
|
||||
if (!local.loaded) {
|
||||
await loadOptions();
|
||||
}
|
||||
const search = local.search.input.toLowerCase();
|
||||
if (search) {
|
||||
local.search.result = options.filter((e) =>
|
||||
e.label.toLowerCase().includes(search)
|
||||
);
|
||||
|
||||
if (
|
||||
local.search.result.length > 0 &&
|
||||
!local.search.result.find(
|
||||
(e) => e.value === local.select?.value
|
||||
)
|
||||
) {
|
||||
}
|
||||
} else {
|
||||
local.search.result = null;
|
||||
}
|
||||
local.search.searching = false;
|
||||
local.render();
|
||||
} else {
|
||||
clearTimeout(local.search.timeout);
|
||||
local.search.timeout = setTimeout(async () => {
|
||||
const result = options_fn?.({
|
||||
search: local.search.input,
|
||||
existing: options,
|
||||
});
|
||||
if (result) {
|
||||
if (result instanceof Promise) {
|
||||
local.search.promise = result;
|
||||
local.search.result = (await result).map((item) => {
|
||||
if (typeof item === "string")
|
||||
return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.search.searching = false;
|
||||
local.search.promise = null;
|
||||
} else {
|
||||
local.search.result = result.map((item) => {
|
||||
if (typeof item === "string")
|
||||
return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.search.searching = false;
|
||||
}
|
||||
|
||||
if (
|
||||
local.search.result.length > 0 &&
|
||||
!local.search.result.find(
|
||||
(e) => e.value === local.select?.value
|
||||
)
|
||||
) {
|
||||
}
|
||||
|
||||
local.render();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onSelect={(value) => {
|
||||
local.open = false;
|
||||
if (!isBetter) local.open = false;
|
||||
resetSearch();
|
||||
const item = options.find((item) => item.value === value);
|
||||
if (item) {
|
||||
|
|
@ -545,9 +653,20 @@ export const Typeahead: FC<{
|
|||
}
|
||||
isMulti={local.mode === "multi"}
|
||||
selected={({ item, options, idx }) => {
|
||||
if (item.value === local.select?.value) {
|
||||
// console.log(local.select);
|
||||
if (isBetter) {
|
||||
const val = local.value?.length ? local.value : [];
|
||||
let isSelect = options.find((e) => {
|
||||
return (
|
||||
e?.value === item?.value &&
|
||||
val.find((ex) => ex === item?.value)
|
||||
);
|
||||
});
|
||||
return isSelect ? true : false;
|
||||
} else if (item.value === local.select?.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
|
|
@ -576,82 +695,51 @@ export const Typeahead: FC<{
|
|||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
placeholder={
|
||||
local.mode === "multi"
|
||||
? placeholder
|
||||
: valueLabel[0]?.label || placeholder
|
||||
}
|
||||
type="text"
|
||||
ref={input}
|
||||
value={inputval}
|
||||
onChange={async (e) => {
|
||||
const val = e.currentTarget.value;
|
||||
if (!local.open) {
|
||||
local.open = true;
|
||||
{isBetter ? (
|
||||
<div className="h-9 flex-grow flex flex-row items-start">
|
||||
<div className="flex flex-grow"></div>
|
||||
<div className="h-9 flex flex-row items-center px-2">
|
||||
<GoChevronDown size={14} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
placeholder={
|
||||
local.mode === "multi"
|
||||
? placeholder
|
||||
: valueLabel[0]?.label || placeholder
|
||||
}
|
||||
type="text"
|
||||
ref={input}
|
||||
value={inputval}
|
||||
onChange={async (e) => {
|
||||
const val = e.currentTarget.value;
|
||||
if (!local.open) {
|
||||
local.open = true;
|
||||
}
|
||||
|
||||
local.search.input = val;
|
||||
local.render();
|
||||
local.search.input = val;
|
||||
local.render();
|
||||
|
||||
if (local.search.promise) {
|
||||
await local.search.promise;
|
||||
}
|
||||
if (local.search.promise) {
|
||||
await local.search.promise;
|
||||
}
|
||||
|
||||
local.search.searching = true;
|
||||
local.render();
|
||||
if (allow_new) {
|
||||
setSearchTerm(val);
|
||||
}
|
||||
if (local.search.searching) {
|
||||
if (local.local_search) {
|
||||
if (!local.loaded) {
|
||||
await loadOptions();
|
||||
}
|
||||
const search = local.search.input.toLowerCase();
|
||||
if (search) {
|
||||
local.search.result = options.filter((e) =>
|
||||
e.label.toLowerCase().includes(search)
|
||||
);
|
||||
|
||||
if (
|
||||
local.search.result.length > 0 &&
|
||||
!local.search.result.find(
|
||||
(e) => e.value === local.select?.value
|
||||
)
|
||||
) {
|
||||
local.select = local.search.result[0];
|
||||
local.search.searching = true;
|
||||
local.render();
|
||||
if (allow_new) {
|
||||
setSearchTerm(val);
|
||||
}
|
||||
if (local.search.searching) {
|
||||
if (local.local_search) {
|
||||
if (!local.loaded) {
|
||||
await loadOptions();
|
||||
}
|
||||
} else {
|
||||
local.search.result = null;
|
||||
}
|
||||
local.search.searching = false;
|
||||
local.render();
|
||||
} else {
|
||||
clearTimeout(local.search.timeout);
|
||||
local.search.timeout = setTimeout(async () => {
|
||||
const result = options_fn?.({
|
||||
search: local.search.input,
|
||||
existing: options,
|
||||
});
|
||||
if (result) {
|
||||
if (result instanceof Promise) {
|
||||
local.search.promise = result;
|
||||
local.search.result = (await result).map((item) => {
|
||||
if (typeof item === "string")
|
||||
return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.search.searching = false;
|
||||
local.search.promise = null;
|
||||
} else {
|
||||
local.search.result = result.map((item) => {
|
||||
if (typeof item === "string")
|
||||
return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.search.searching = false;
|
||||
}
|
||||
const search = local.search.input.toLowerCase();
|
||||
if (search) {
|
||||
local.search.result = options.filter((e) =>
|
||||
e.label.toLowerCase().includes(search)
|
||||
);
|
||||
|
||||
if (
|
||||
local.search.result.length > 0 &&
|
||||
|
|
@ -661,24 +749,64 @@ export const Typeahead: FC<{
|
|||
) {
|
||||
local.select = local.search.result[0];
|
||||
}
|
||||
|
||||
local.render();
|
||||
} else {
|
||||
local.search.result = null;
|
||||
}
|
||||
}, 100);
|
||||
local.search.searching = false;
|
||||
local.render();
|
||||
} else {
|
||||
clearTimeout(local.search.timeout);
|
||||
local.search.timeout = setTimeout(async () => {
|
||||
const result = options_fn?.({
|
||||
search: local.search.input,
|
||||
existing: options,
|
||||
});
|
||||
if (result) {
|
||||
if (result instanceof Promise) {
|
||||
local.search.promise = result;
|
||||
local.search.result = (await result).map((item) => {
|
||||
if (typeof item === "string")
|
||||
return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.search.searching = false;
|
||||
local.search.promise = null;
|
||||
} else {
|
||||
local.search.result = result.map((item) => {
|
||||
if (typeof item === "string")
|
||||
return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.search.searching = false;
|
||||
}
|
||||
|
||||
if (
|
||||
local.search.result.length > 0 &&
|
||||
!local.search.result.find(
|
||||
(e) => e.value === local.select?.value
|
||||
)
|
||||
) {
|
||||
local.select = local.search.result[0];
|
||||
}
|
||||
|
||||
local.render();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={!disabled ? disabledSearch : disabled}
|
||||
spellCheck={false}
|
||||
className={cx(
|
||||
"text-black flex h-9 w-full border-input bg-transparent px-3 py-1 text-base border-none shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground md:text-sm focus:outline-none focus:ring-0",
|
||||
local.mode === "single" ? "cursor-pointer" : ""
|
||||
)}
|
||||
style={{
|
||||
pointerEvents: disabledSearch ? "none" : "auto", // Mencegah input menangkap klik saat disabled
|
||||
}}
|
||||
onKeyDown={keydown}
|
||||
/>
|
||||
}}
|
||||
disabled={!disabled ? disabledSearch : disabled}
|
||||
spellCheck={false}
|
||||
className={cx(
|
||||
"text-black flex h-9 w-full border-input bg-transparent px-3 py-1 text-base border-none shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground md:text-sm focus:outline-none focus:ring-0",
|
||||
local.mode === "single" ? "cursor-pointer" : ""
|
||||
)}
|
||||
style={{
|
||||
pointerEvents: disabledSearch ? "none" : "auto", // Mencegah input menangkap klik saat disabled
|
||||
}}
|
||||
onKeyDown={keydown}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TypeaheadOptions>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,16 @@ import { FC } from "react";
|
|||
import { useLocal } from "@/lib/utils/use-local";
|
||||
import { Popover } from "../../Popover/Popover";
|
||||
import { ButtonBetter } from "../../ui/button";
|
||||
import { Checkbox } from "../../ui/checkbox";
|
||||
import { ScrollArea } from "../../ui/scroll-area";
|
||||
import { IoCheckmark, IoSearchOutline } from "react-icons/io5";
|
||||
|
||||
export type OptionItem = { value: string; label: string };
|
||||
export const TypeaheadOptions: FC<{
|
||||
popup?: boolean;
|
||||
onRemove?: (data: any) => void;
|
||||
onSelectAll?: (data: boolean) => void;
|
||||
init?: any;
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
children: any;
|
||||
|
|
@ -24,6 +30,9 @@ export const TypeaheadOptions: FC<{
|
|||
width?: number;
|
||||
isMulti?: boolean;
|
||||
fitur?: "search-add";
|
||||
isBetter?: boolean;
|
||||
onSearch?: (event: any) => void;
|
||||
search?: boolean;
|
||||
}> = ({
|
||||
popup,
|
||||
loading,
|
||||
|
|
@ -40,6 +49,12 @@ export const TypeaheadOptions: FC<{
|
|||
width,
|
||||
isMulti,
|
||||
fitur,
|
||||
isBetter,
|
||||
init,
|
||||
onSearch,
|
||||
search,
|
||||
onRemove,
|
||||
onSelectAll,
|
||||
}) => {
|
||||
if (!popup) return children;
|
||||
const local = useLocal({
|
||||
|
|
@ -50,12 +65,20 @@ export const TypeaheadOptions: FC<{
|
|||
<div
|
||||
className={cx(
|
||||
className,
|
||||
width
|
||||
"flex flex-col",
|
||||
isBetter
|
||||
? css`
|
||||
min-width: 350px;
|
||||
height: 450px;
|
||||
`
|
||||
: width
|
||||
? css`
|
||||
min-width: ${width}px;
|
||||
height: 450px;
|
||||
`
|
||||
: css`
|
||||
min-width: 150px;
|
||||
height: 450px;
|
||||
`,
|
||||
css`
|
||||
max-height: 400px;
|
||||
|
|
@ -63,45 +86,74 @@ export const TypeaheadOptions: FC<{
|
|||
`
|
||||
)}
|
||||
>
|
||||
{!loading ? (
|
||||
{isBetter ? (
|
||||
<>
|
||||
{options.map((item, idx) => {
|
||||
const is_selected = selected?.({ item, options, idx });
|
||||
|
||||
if (is_selected) {
|
||||
local.selectedIdx = idx;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row w-full p-1">
|
||||
<div className="flex-grow flex flex-row relative">
|
||||
<div
|
||||
tabIndex={0}
|
||||
key={item.value + "_" + idx}
|
||||
className={cx(
|
||||
"opt-item px-3 py-1 cursor-pointer option-item text-sm",
|
||||
is_selected ? "bg-blue-600 text-white" : "hover:bg-blue-50",
|
||||
idx > 0 && "border-t"
|
||||
"absolute left-0 px-1.5",
|
||||
css`
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`
|
||||
)}
|
||||
onClick={() => {
|
||||
onSelect?.(item.value);
|
||||
}}
|
||||
>
|
||||
{item.label || <> </>}
|
||||
<IoSearchOutline />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<input
|
||||
placeholder={"Search"}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
onChange={onSearch}
|
||||
className={cx(
|
||||
"pl-6 pr-3 py-1 rounded-md text-black flex h-9 flex-grow border border-gray-200 bg-white text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground md:text-sm focus:outline-none focus:ring-0"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{init.search.input === "" || !init.search.input ? (
|
||||
<div className="flex flex-row px-3 py-1 gap-x-2 items-center cursor-pointer">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
className="border border-primary"
|
||||
checked={init?.selectBetter?.all ? true : false}
|
||||
onClick={(e) => {
|
||||
init.selectBetter.all = !init.selectBetter.all;
|
||||
init.render();
|
||||
if (typeof onSelectAll === "function")
|
||||
onSelectAll(init.selectBetter.all);
|
||||
}}
|
||||
/>
|
||||
Select All
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{loading || searching ? (
|
||||
<div className="px-4 w-full text-slate-400 text-sm py-2">
|
||||
<div
|
||||
className={cx(
|
||||
isBetter && "flex-grow flex flex-row items-center justify-center",
|
||||
"px-4 w-full text-slate-400 text-sm py-2"
|
||||
)}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{options.length === 0 && (
|
||||
<div className="p-4 w-full text-center text-md text-slate-400">
|
||||
{options.length === 0 ? (
|
||||
<div
|
||||
className={cx(
|
||||
isBetter &&
|
||||
"flex-grow flex flex-row items-center justify-center",
|
||||
"p-4 w-full text-center text-md text-slate-400"
|
||||
)}
|
||||
>
|
||||
{fitur === "search-add" ? (
|
||||
<ButtonBetter
|
||||
variant={"outline"}
|
||||
|
|
@ -155,9 +207,73 @@ export const TypeaheadOptions: FC<{
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="w-full flex-grow flex flex-col gap-y-2">
|
||||
{options.map((item, idx) => {
|
||||
const is_selected = isBetter
|
||||
? init.selectBetter.all
|
||||
? true
|
||||
: selected?.({ item, options, idx })
|
||||
: selected?.({ item, options, idx });
|
||||
|
||||
if (is_selected) {
|
||||
local.selectedIdx = idx;
|
||||
}
|
||||
if (isBetter) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row px-3 py-1 gap-x-2 items-center cursor-pointer"
|
||||
key={item.value + "_" + idx}
|
||||
>
|
||||
<Checkbox
|
||||
id="terms"
|
||||
className="border border-primary"
|
||||
checked={is_selected}
|
||||
onClick={() => {
|
||||
if (is_selected) {
|
||||
if (typeof onRemove === "function") onRemove(item);
|
||||
} else {
|
||||
onSelect?.(item.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{item.label || <> </>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
key={item.value + "_" + idx}
|
||||
className={cx(
|
||||
"opt-item px-3 py-1 cursor-pointer option-item text-sm",
|
||||
is_selected
|
||||
? "bg-blue-600 text-white"
|
||||
: "hover:bg-blue-50",
|
||||
idx > 0 && "border-t"
|
||||
)}
|
||||
onClick={() => {
|
||||
onSelect?.(item.value);
|
||||
}}
|
||||
>
|
||||
{item.label || <> </>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isBetter ? (
|
||||
<div className="w-full flex flex-row items-center justify-end p-1 border-t border-gray-200">
|
||||
<ButtonBetter className="rounded-md text-xs flex flex-row items-center gap-x-1">
|
||||
<IoCheckmark />
|
||||
OK
|
||||
</ButtonBetter>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -169,7 +285,7 @@ export const TypeaheadOptions: FC<{
|
|||
arrow={false}
|
||||
onOpenChange={onOpenChange}
|
||||
backdrop={false}
|
||||
classNameTrigger={!isMulti ? "w-full" : ""}
|
||||
classNameTrigger={!isMulti ? "w-full" : "flex-grow"}
|
||||
placement="bottom-start"
|
||||
className="flex-1 rounded-md overflow-hidden"
|
||||
content={content}
|
||||
|
|
|
|||
Loading…
Reference in New Issue