mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Show/allow unicode domain name
This commit is contained in:
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Redirecting to login...",
|
"autoLoginRedirecting": "Redirecting to login...",
|
||||||
"autoLoginError": "Auto Login Error",
|
"autoLoginError": "Auto Login Error",
|
||||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL."
|
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||||
|
"internationaldomaindetected": "International Domain Detected",
|
||||||
|
"willbestoredas": "Will be stored as:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Redirecting to login...",
|
"autoLoginRedirecting": "Redirecting to login...",
|
||||||
"autoLoginError": "Auto Login Error",
|
"autoLoginError": "Auto Login Error",
|
||||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL."
|
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||||
|
"internationaldomaindetected": "Detekována mezinárodní doména",
|
||||||
|
"willbestoredas": "Bude uloženo jako:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Weiterleitung zur Anmeldung...",
|
"autoLoginRedirecting": "Weiterleitung zur Anmeldung...",
|
||||||
"autoLoginError": "Fehler bei der automatischen Anmeldung",
|
"autoLoginError": "Fehler bei der automatischen Anmeldung",
|
||||||
"autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.",
|
"autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.",
|
||||||
"autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL."
|
"autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.",
|
||||||
|
"internationaldomaindetected": "Internationale Domäne erkannt",
|
||||||
|
"willbestoredas": "Wird gespeichert als:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1494,5 +1494,7 @@
|
|||||||
"documentation": "documentation"
|
"documentation": "documentation"
|
||||||
},
|
},
|
||||||
"convertButton": "Convert This Node to Managed Self-Hosted"
|
"convertButton": "Convert This Node to Managed Self-Hosted"
|
||||||
}
|
},
|
||||||
|
"internationaldomaindetected": "International Domain Detected",
|
||||||
|
"willbestoredas": "Will be stored as:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Redirigiendo al inicio de sesión...",
|
"autoLoginRedirecting": "Redirigiendo al inicio de sesión...",
|
||||||
"autoLoginError": "Error de inicio de sesión automático",
|
"autoLoginError": "Error de inicio de sesión automático",
|
||||||
"autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.",
|
"autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.",
|
||||||
"autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación."
|
"autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación.",
|
||||||
|
"internationaldomaindetected": "Dominio internacional detectado",
|
||||||
|
"willbestoredas": "Se almacenará como: "
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Redirection vers la connexion...",
|
"autoLoginRedirecting": "Redirection vers la connexion...",
|
||||||
"autoLoginError": "Erreur de connexion automatique",
|
"autoLoginError": "Erreur de connexion automatique",
|
||||||
"autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.",
|
"autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.",
|
||||||
"autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification."
|
"autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.",
|
||||||
|
"internationaldomaindetected": "Domaine international détecté",
|
||||||
|
"willbestoredas": "Sera stocké comme:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Reindirizzamento al login...",
|
"autoLoginRedirecting": "Reindirizzamento al login...",
|
||||||
"autoLoginError": "Errore di Accesso Automatico",
|
"autoLoginError": "Errore di Accesso Automatico",
|
||||||
"autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.",
|
"autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.",
|
||||||
"autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione."
|
"autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.",
|
||||||
|
"internationaldomaindetected": "Rilevato dominio internazionale",
|
||||||
|
"willbestoredas": "Verrà archiviato come:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "로그인으로 리디렉션 중...",
|
"autoLoginRedirecting": "로그인으로 리디렉션 중...",
|
||||||
"autoLoginError": "자동 로그인 오류",
|
"autoLoginError": "자동 로그인 오류",
|
||||||
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
|
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
|
||||||
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패."
|
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
|
||||||
|
"internationaldomaindetected": "국제 도메인 감지됨",
|
||||||
|
"willbestoredas": "다음과 같이 저장됩니다."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Redirecting naar inloggen...",
|
"autoLoginRedirecting": "Redirecting naar inloggen...",
|
||||||
"autoLoginError": "Auto Login Fout",
|
"autoLoginError": "Auto Login Fout",
|
||||||
"autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.",
|
"autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt."
|
"autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.",
|
||||||
|
"internationaldomaindetected": "Internationaal Domein Gedetecteerd",
|
||||||
|
"willbestoredas": "Wordt opgeslagen als:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Przekierowanie do logowania...",
|
"autoLoginRedirecting": "Przekierowanie do logowania...",
|
||||||
"autoLoginError": "Błąd automatycznego logowania",
|
"autoLoginError": "Błąd automatycznego logowania",
|
||||||
"autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.",
|
"autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.",
|
||||||
"autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania."
|
"autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania.",
|
||||||
|
"internationaldomaindetected": "Wykryto domenę międzynarodową",
|
||||||
|
"willbestoredas": "Będzie przechowywane jako:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Redirecionando para login...",
|
"autoLoginRedirecting": "Redirecionando para login...",
|
||||||
"autoLoginError": "Erro de Login Automático",
|
"autoLoginError": "Erro de Login Automático",
|
||||||
"autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.",
|
"autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.",
|
||||||
"autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação."
|
"autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.",
|
||||||
|
"internationaldomaindetected": "Domínio internacional detetado",
|
||||||
|
"willbestoredas": "Será armazenado como:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Перенаправление к входу...",
|
"autoLoginRedirecting": "Перенаправление к входу...",
|
||||||
"autoLoginError": "Ошибка автоматического входа",
|
"autoLoginError": "Ошибка автоматического входа",
|
||||||
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
|
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
|
||||||
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации."
|
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
|
||||||
|
"internationaldomaindetected": "Обнаружен международный домен",
|
||||||
|
"willbestoredas": "Будет сохранен как:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "Girişe yönlendiriliyorsunuz...",
|
"autoLoginRedirecting": "Girişe yönlendiriliyorsunuz...",
|
||||||
"autoLoginError": "Otomatik Giriş Hatası",
|
"autoLoginError": "Otomatik Giriş Hatası",
|
||||||
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
|
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
|
||||||
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı."
|
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.",
|
||||||
|
"internationaldomaindetected": "Uluslararası Etki Alanı Algılandı",
|
||||||
|
"willbestoredas": "Şu şekilde saklanacaktır:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1457,5 +1457,7 @@
|
|||||||
"autoLoginRedirecting": "重定向到登录...",
|
"autoLoginRedirecting": "重定向到登录...",
|
||||||
"autoLoginError": "自动登录错误",
|
"autoLoginError": "自动登录错误",
|
||||||
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
|
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
|
||||||
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。"
|
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。",
|
||||||
|
"internationaldomaindetected": "检测到国际域名",
|
||||||
|
"willbestoredas": "将存储为:"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import {
|
|||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage,
|
||||||
|
FormDescription
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { useToast } from "@app/hooks/useToast";
|
import { useToast } from "@app/hooks/useToast";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
@@ -33,7 +34,7 @@ import { CreateDomainResponse } from "@server/routers/domain/createOrgDomain";
|
|||||||
import { StrategySelect } from "@app/components/StrategySelect";
|
import { StrategySelect } from "@app/components/StrategySelect";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { InfoIcon, AlertTriangle } from "lucide-react";
|
import { InfoIcon, AlertTriangle, Globe } from "lucide-react";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
import {
|
import {
|
||||||
InfoSection,
|
InfoSection,
|
||||||
@@ -43,9 +44,58 @@ import {
|
|||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { toASCII, toUnicode } from 'punycode';
|
||||||
|
|
||||||
|
|
||||||
|
// Helper functions for Unicode domain handling
|
||||||
|
function toPunycode(domain: string): string {
|
||||||
|
try {
|
||||||
|
const parts = toASCII(domain);
|
||||||
|
return parts;
|
||||||
|
} catch (error) {
|
||||||
|
return domain.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromPunycode(domain: string): string {
|
||||||
|
try {
|
||||||
|
const parts = toUnicode(domain)
|
||||||
|
return parts;
|
||||||
|
} catch (error) {
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidDomainFormat(domain: string): boolean {
|
||||||
|
const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/;
|
||||||
|
|
||||||
|
if (!unicodeRegex.test(domain)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = domain.split('.');
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (part.length > 63) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.length > 253) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
baseDomain: z.string().min(1, "Domain is required"),
|
baseDomain: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Domain is required")
|
||||||
|
.refine((val) => isValidDomainFormat(val), "Invalid domain format")
|
||||||
|
.transform((val) => toPunycode(val)),
|
||||||
type: z.enum(["ns", "cname", "wildcard"])
|
type: z.enum(["ns", "cname", "wildcard"])
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,8 +159,14 @@ export default function CreateDomainForm({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const domainType = form.watch("type");
|
|
||||||
const baseDomain = form.watch("baseDomain");
|
const baseDomain = form.watch("baseDomain");
|
||||||
|
const domainInputValue = form.watch("baseDomain") || "";
|
||||||
|
|
||||||
|
const punycodePreview = useMemo(() => {
|
||||||
|
if (!domainInputValue) return "";
|
||||||
|
const punycode = toPunycode(domainInputValue);
|
||||||
|
return punycode !== domainInputValue.toLowerCase() ? punycode : "";
|
||||||
|
}, [domainInputValue]);
|
||||||
|
|
||||||
let domainOptions: any = [];
|
let domainOptions: any = [];
|
||||||
if (build == "enterprise" || build == "saas") {
|
if (build == "enterprise" || build == "saas") {
|
||||||
@@ -182,10 +238,23 @@ export default function CreateDomainForm({
|
|||||||
<FormLabel>{t("domain")}</FormLabel>
|
<FormLabel>{t("domain")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="example.com"
|
placeholder="example.com, café.com, 日本.com"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{punycodePreview && (
|
||||||
|
<FormDescription className="flex items-center gap-2 text-xs">
|
||||||
|
<Alert>
|
||||||
|
<Globe className="h-4 w-4" />
|
||||||
|
<AlertTitle>{t("internationaldomaindetected")}</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
<p>{t("willbestoredas")} <code className="font-mono px-1 py-0.5 rounded">{punycodePreview}</code></p>
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -206,66 +275,73 @@ export default function CreateDomainForm({
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{createdDomain.nsRecords &&
|
{createdDomain.nsRecords &&
|
||||||
createdDomain.nsRecords.length > 0 && (
|
createdDomain.nsRecords.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium mb-3">
|
<h3 className="font-medium mb-3">
|
||||||
{t("createDomainNsRecords")}
|
{t("createDomainNsRecords")}
|
||||||
</h3>
|
</h3>
|
||||||
<InfoSections cols={1}>
|
<InfoSections cols={1}>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
{t("createDomainRecord")}
|
{t("createDomainRecord")}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{t(
|
{t(
|
||||||
"createDomainType"
|
"createDomainType"
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-mono">
|
<span className="text-sm font-mono">
|
||||||
NS
|
NS
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{t(
|
{t(
|
||||||
"createDomainName"
|
"createDomainName"
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-mono">
|
<div className="text-right">
|
||||||
{baseDomain}
|
<span className="text-sm font-mono block">
|
||||||
</span>
|
{fromPunycode(baseDomain)}
|
||||||
</div>
|
</span>
|
||||||
<span className="text-sm font-medium">
|
{fromPunycode(baseDomain) !== baseDomain && (
|
||||||
{t(
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
"createDomainValue"
|
({baseDomain})
|
||||||
)}
|
</span>
|
||||||
</span>
|
)}
|
||||||
{createdDomain.nsRecords.map(
|
|
||||||
(
|
|
||||||
nsRecord,
|
|
||||||
index
|
|
||||||
) => (
|
|
||||||
<div
|
|
||||||
className="flex justify-between items-center"
|
|
||||||
key={index}
|
|
||||||
>
|
|
||||||
<CopyToClipboard
|
|
||||||
text={
|
|
||||||
nsRecord
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
)}
|
<span className="text-sm font-medium">
|
||||||
</div>
|
{t(
|
||||||
</InfoSectionContent>
|
"createDomainValue"
|
||||||
</InfoSection>
|
)}
|
||||||
</InfoSections>
|
</span>
|
||||||
</div>
|
{createdDomain.nsRecords.map(
|
||||||
)}
|
(
|
||||||
|
nsRecord,
|
||||||
|
index
|
||||||
|
) => (
|
||||||
|
<div
|
||||||
|
className="flex justify-between items-center"
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<CopyToClipboard
|
||||||
|
text={
|
||||||
|
nsRecord
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
</InfoSections>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{createdDomain.cnameRecords &&
|
{createdDomain.cnameRecords &&
|
||||||
createdDomain.cnameRecords.length > 0 && (
|
createdDomain.cnameRecords.length > 0 && (
|
||||||
@@ -307,11 +383,16 @@ export default function CreateDomainForm({
|
|||||||
"createDomainName"
|
"createDomainName"
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-mono">
|
<div className="text-right">
|
||||||
{
|
<span className="text-sm font-mono block">
|
||||||
cnameRecord.baseDomain
|
{fromPunycode(cnameRecord.baseDomain)}
|
||||||
}
|
</span>
|
||||||
</span>
|
{fromPunycode(cnameRecord.baseDomain) !== cnameRecord.baseDomain && (
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
|
({cnameRecord.baseDomain})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
@@ -374,11 +455,16 @@ export default function CreateDomainForm({
|
|||||||
"createDomainName"
|
"createDomainName"
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-mono">
|
<div className="text-right">
|
||||||
{
|
<span className="text-sm font-mono block">
|
||||||
aRecord.baseDomain
|
{fromPunycode(aRecord.baseDomain)}
|
||||||
}
|
</span>
|
||||||
</span>
|
{fromPunycode(aRecord.baseDomain) !== aRecord.baseDomain && (
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
|
({aRecord.baseDomain})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
@@ -390,7 +476,7 @@ export default function CreateDomainForm({
|
|||||||
{
|
{
|
||||||
aRecord.value
|
aRecord.value
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
@@ -440,11 +526,16 @@ export default function CreateDomainForm({
|
|||||||
"createDomainName"
|
"createDomainName"
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-mono">
|
<div className="text-right">
|
||||||
{
|
<span className="text-sm font-mono block">
|
||||||
txtRecord.baseDomain
|
{fromPunycode(txtRecord.baseDomain)}
|
||||||
}
|
</span>
|
||||||
</span>
|
{fromPunycode(txtRecord.baseDomain) !== txtRecord.baseDomain && (
|
||||||
|
<span className="text-xs text-muted-foreground font-mono">
|
||||||
|
({txtRecord.baseDomain})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
@@ -513,4 +604,4 @@ export default function CreateDomainForm({
|
|||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import { GetOrgResponse } from "@server/routers/org";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import OrgProvider from "@app/providers/OrgProvider";
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import { ListDomainsResponse } from "@server/routers/domain";
|
import { ListDomainsResponse } from "@server/routers/domain";
|
||||||
|
import { toUnicode } from 'punycode';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
@@ -22,7 +23,13 @@ export default async function DomainsPage(props: Props) {
|
|||||||
const res = await internal.get<
|
const res = await internal.get<
|
||||||
AxiosResponse<ListDomainsResponse>
|
AxiosResponse<ListDomainsResponse>
|
||||||
>(`/org/${params.orgId}/domains`, await authCookieHeader());
|
>(`/org/${params.orgId}/domains`, await authCookieHeader());
|
||||||
domains = res.data.data.domains as DomainRow[];
|
|
||||||
|
const rawDomains = res.data.data.domains as DomainRow[];
|
||||||
|
|
||||||
|
domains = rawDomains.map((domain) => ({
|
||||||
|
...domain,
|
||||||
|
baseDomain: toUnicode(domain.baseDomain),
|
||||||
|
}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { toUnicode } from "punycode";
|
||||||
|
|
||||||
interface DomainOption {
|
interface DomainOption {
|
||||||
baseDomain: string;
|
baseDomain: string;
|
||||||
@@ -91,7 +92,7 @@ export default function CustomDomainInput({
|
|||||||
key={option.domainId}
|
key={option.domainId}
|
||||||
value={option.domainId}
|
value={option.domainId}
|
||||||
>
|
>
|
||||||
.{option.baseDomain}
|
.{toUnicode(option.baseDomain)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -12,15 +12,19 @@ import {
|
|||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { toUnicode } from 'punycode';
|
||||||
|
|
||||||
type ResourceInfoBoxType = {};
|
type ResourceInfoBoxType = {};
|
||||||
|
|
||||||
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
||||||
const { resource, authInfo } = useResourceContext();
|
const { resource, authInfo } = useResourceContext();
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
|
||||||
|
const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert>
|
<Alert>
|
||||||
@@ -34,9 +38,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
|||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{authInfo.password ||
|
{authInfo.password ||
|
||||||
authInfo.pincode ||
|
authInfo.pincode ||
|
||||||
authInfo.sso ||
|
authInfo.sso ||
|
||||||
authInfo.whitelist ? (
|
authInfo.whitelist ? (
|
||||||
<div className="flex items-start space-x-2 text-green-500">
|
<div className="flex items-start space-x-2 text-green-500">
|
||||||
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
||||||
<span>{t("protected")}</span>
|
<span>{t("protected")}</span>
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ import DomainPicker from "@app/components/DomainPicker";
|
|||||||
import { Globe } from "lucide-react";
|
import { Globe } from "lucide-react";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||||
|
import { DomainRow } from "../../../domains/DomainsTable";
|
||||||
|
import { toUnicode } from "punycode";
|
||||||
|
|
||||||
export default function GeneralForm() {
|
export default function GeneralForm() {
|
||||||
const [formKey, setFormKey] = useState(0);
|
const [formKey, setFormKey] = useState(0);
|
||||||
@@ -155,7 +157,11 @@ export default function GeneralForm() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res?.status === 200) {
|
if (res?.status === 200) {
|
||||||
const domains = res.data.data.domains;
|
const rawDomains = res.data.data.domains as DomainRow[];
|
||||||
|
const domains = rawDomains.map((domain) => ({
|
||||||
|
...domain,
|
||||||
|
baseDomain: toUnicode(domain.baseDomain),
|
||||||
|
}));
|
||||||
setBaseDomains(domains);
|
setBaseDomains(domains);
|
||||||
setFormKey((key) => key + 1);
|
setFormKey((key) => key + 1);
|
||||||
}
|
}
|
||||||
@@ -319,10 +325,10 @@ export default function GeneralForm() {
|
|||||||
.target
|
.target
|
||||||
.value
|
.value
|
||||||
? parseInt(
|
? parseInt(
|
||||||
e
|
e
|
||||||
.target
|
.target
|
||||||
.value
|
.value
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ import { isTargetValid } from "@server/lib/validators";
|
|||||||
import { ListTargetsResponse } from "@server/routers/target";
|
import { ListTargetsResponse } from "@server/routers/target";
|
||||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
import { DockerManager, DockerState } from "@app/lib/docker";
|
||||||
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
||||||
|
import { toUnicode } from 'punycode';
|
||||||
|
import { DomainRow } from "../../domains/DomainsTable";
|
||||||
|
|
||||||
const baseResourceFormSchema = z.object({
|
const baseResourceFormSchema = z.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
@@ -469,7 +471,11 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res?.status === 200) {
|
if (res?.status === 200) {
|
||||||
const domains = res.data.data.domains;
|
const rawDomains = res.data.data.domains as DomainRow[];
|
||||||
|
const domains = rawDomains.map((domain) => ({
|
||||||
|
...domain,
|
||||||
|
baseDomain: toUnicode(domain.baseDomain),
|
||||||
|
}));
|
||||||
setBaseDomains(domains);
|
setBaseDomains(domains);
|
||||||
// if (domains.length) {
|
// if (domains.length) {
|
||||||
// httpForm.setValue("domainId", domains[0].domainId);
|
// httpForm.setValue("domainId", domains[0].domainId);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { GetOrgResponse } from "@server/routers/org";
|
|||||||
import OrgProvider from "@app/providers/OrgProvider";
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import { toUnicode } from "punycode";
|
||||||
|
|
||||||
type ResourcesPageProps = {
|
type ResourcesPageProps = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
@@ -75,7 +76,9 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
|||||||
id: resource.resourceId,
|
id: resource.resourceId,
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
|
|
||||||
|
|
||||||
|
domain: `${resource.ssl ? "https://" : "http://"}${toUnicode(resource.fullDomain || "")}`,
|
||||||
protocol: resource.protocol,
|
protocol: resource.protocol,
|
||||||
proxyPort: resource.proxyPort,
|
proxyPort: resource.proxyPort,
|
||||||
http: resource.http,
|
http: resource.http,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ import {
|
|||||||
} from "@app/components/ui/collapsible";
|
} from "@app/components/ui/collapsible";
|
||||||
import AccessTokenSection from "./AccessTokenUsage";
|
import AccessTokenSection from "./AccessTokenUsage";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { toUnicode } from 'punycode';
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -159,7 +160,7 @@ export default function CreateShareLinkForm({
|
|||||||
.map((r) => ({
|
.map((r) => ({
|
||||||
resourceId: r.resourceId,
|
resourceId: r.resourceId,
|
||||||
name: r.name,
|
name: r.name,
|
||||||
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`
|
resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/`
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
validateByDomainType,
|
validateByDomainType,
|
||||||
isValidSubdomainStructure
|
isValidSubdomainStructure
|
||||||
} from "@/lib/subdomain-utils";
|
} from "@/lib/subdomain-utils";
|
||||||
|
import { toUnicode } from "punycode";
|
||||||
|
|
||||||
type OrganizationDomain = {
|
type OrganizationDomain = {
|
||||||
domainId: string;
|
domainId: string;
|
||||||
@@ -126,6 +127,7 @@ export default function DomainPicker2({
|
|||||||
)
|
)
|
||||||
.map((domain) => ({
|
.map((domain) => ({
|
||||||
...domain,
|
...domain,
|
||||||
|
baseDomain: toUnicode(domain.baseDomain),
|
||||||
type: domain.type as "ns" | "cname" | "wildcard"
|
type: domain.type as "ns" | "cname" | "wildcard"
|
||||||
}));
|
}));
|
||||||
setOrganizationDomains(domains);
|
setOrganizationDomains(domains);
|
||||||
@@ -406,6 +408,12 @@ export default function DomainPicker2({
|
|||||||
const hasMoreProvided =
|
const hasMoreProvided =
|
||||||
sortedAvailableOptions.length > providedDomainsShown;
|
sortedAvailableOptions.length > providedDomainsShown;
|
||||||
|
|
||||||
|
|
||||||
|
const isValidDomainCharacter = (char: string) => {
|
||||||
|
// Allow Unicode letters, numbers, hyphens, and periods
|
||||||
|
return /[\p{L}\p{N}.-]/u.test(char);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
@@ -424,8 +432,8 @@ export default function DomainPicker2({
|
|||||||
showProvidedDomainSearch
|
showProvidedDomainSearch
|
||||||
? ""
|
? ""
|
||||||
: showSubdomainInput
|
: showSubdomainInput
|
||||||
? ""
|
? ""
|
||||||
: t("domainPickerNotAvailableForCname")
|
: t("domainPickerNotAvailableForCname")
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
!showSubdomainInput && !showProvidedDomainSearch
|
!showSubdomainInput && !showProvidedDomainSearch
|
||||||
@@ -436,10 +444,16 @@ export default function DomainPicker2({
|
|||||||
"border-red-500 focus:border-red-500"
|
"border-red-500 focus:border-red-500"
|
||||||
)}
|
)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
const rawInput = e.target.value;
|
||||||
|
const validInput = rawInput
|
||||||
|
.split("")
|
||||||
|
.filter((char) => isValidDomainCharacter(char))
|
||||||
|
.join("");
|
||||||
|
|
||||||
if (showProvidedDomainSearch) {
|
if (showProvidedDomainSearch) {
|
||||||
handleProvidedDomainInputChange(e.target.value);
|
handleProvidedDomainInputChange(validInput);
|
||||||
} else {
|
} else {
|
||||||
handleSubdomainChange(e.target.value);
|
handleSubdomainChange(validInput);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -448,7 +462,6 @@ export default function DomainPicker2({
|
|||||||
This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.
|
This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showSubdomainInput && !subdomainInput && (
|
{showSubdomainInput && !subdomainInput && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("domainPickerEnterSubdomainOrLeaveBlank")}
|
{t("domainPickerEnterSubdomainOrLeaveBlank")}
|
||||||
@@ -474,7 +487,7 @@ export default function DomainPicker2({
|
|||||||
{selectedBaseDomain ? (
|
{selectedBaseDomain ? (
|
||||||
<div className="flex items-center space-x-2 min-w-0 flex-1">
|
<div className="flex items-center space-x-2 min-w-0 flex-1">
|
||||||
{selectedBaseDomain.type ===
|
{selectedBaseDomain.type ===
|
||||||
"organization" ? null : (
|
"organization" ? null : (
|
||||||
<Zap className="h-4 w-4 flex-shrink-0" />
|
<Zap className="h-4 w-4 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
@@ -568,67 +581,67 @@ export default function DomainPicker2({
|
|||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
{(build === "saas" ||
|
{(build === "saas" ||
|
||||||
build === "enterprise") && (
|
build === "enterprise") && (
|
||||||
<CommandSeparator className="my-2" />
|
<CommandSeparator className="my-2" />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(build === "saas" ||
|
{(build === "saas" ||
|
||||||
build === "enterprise") && (
|
build === "enterprise") && (
|
||||||
<CommandGroup
|
<CommandGroup
|
||||||
heading={
|
heading={
|
||||||
build === "enterprise"
|
build === "enterprise"
|
||||||
? t(
|
? t(
|
||||||
"domainPickerProvidedDomains"
|
"domainPickerProvidedDomains"
|
||||||
)
|
)
|
||||||
: t("domainPickerFreeDomains")
|
: t("domainPickerFreeDomains")
|
||||||
}
|
}
|
||||||
className="py-2"
|
className="py-2"
|
||||||
>
|
>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key="provided-search"
|
key="provided-search"
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
handleBaseDomainSelect({
|
handleBaseDomainSelect({
|
||||||
id: "provided-search",
|
id: "provided-search",
|
||||||
domain:
|
domain:
|
||||||
build ===
|
build ===
|
||||||
"enterprise"
|
"enterprise"
|
||||||
|
? "Provided Domain"
|
||||||
|
: "Free Provided Domain",
|
||||||
|
type: "provided-search"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="mx-2 rounded-md"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3">
|
||||||
|
<Zap className="h-4 w-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
|
<span className="font-medium truncate">
|
||||||
|
{build === "enterprise"
|
||||||
? "Provided Domain"
|
? "Provided Domain"
|
||||||
: "Free Provided Domain",
|
: "Free Provided Domain"}
|
||||||
type: "provided-search"
|
</span>
|
||||||
})
|
<span className="text-xs text-muted-foreground">
|
||||||
}
|
{t(
|
||||||
className="mx-2 rounded-md"
|
"domainPickerSearchForAvailableDomains"
|
||||||
>
|
)}
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3">
|
</span>
|
||||||
<Zap className="h-4 w-4 text-primary" />
|
</div>
|
||||||
</div>
|
<Check
|
||||||
<div className="flex flex-col flex-1 min-w-0">
|
className={cn(
|
||||||
<span className="font-medium truncate">
|
"h-4 w-4 text-primary",
|
||||||
{build === "enterprise"
|
selectedBaseDomain?.id ===
|
||||||
? "Provided Domain"
|
"provided-search"
|
||||||
: "Free Provided Domain"}
|
? "opacity-100"
|
||||||
</span>
|
: "opacity-0"
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"domainPickerSearchForAvailableDomains"
|
|
||||||
)}
|
)}
|
||||||
</span>
|
/>
|
||||||
</div>
|
</CommandItem>
|
||||||
<Check
|
</CommandList>
|
||||||
className={cn(
|
</CommandGroup>
|
||||||
"h-4 w-4 text-primary",
|
)}
|
||||||
selectedBaseDomain?.id ===
|
|
||||||
"provided-search"
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CommandItem>
|
|
||||||
</CommandList>
|
|
||||||
</CommandGroup>
|
|
||||||
)}
|
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -684,7 +697,7 @@ export default function DomainPicker2({
|
|||||||
htmlFor={option.domainNamespaceId}
|
htmlFor={option.domainNamespaceId}
|
||||||
data-state={
|
data-state={
|
||||||
selectedProvidedDomain?.domainNamespaceId ===
|
selectedProvidedDomain?.domainNamespaceId ===
|
||||||
option.domainNamespaceId
|
option.domainNamespaceId
|
||||||
? "checked"
|
? "checked"
|
||||||
: "unchecked"
|
: "unchecked"
|
||||||
}
|
}
|
||||||
@@ -764,4 +777,4 @@ function debounce<T extends (...args: any[]) => any>(
|
|||||||
func(...args);
|
func(...args);
|
||||||
}, wait);
|
}, wait);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user