mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
feat(ui): NotificationProviderPicker
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar, Select, type SelectProps, Typography, theme } from "antd";
|
||||
import { Avatar, Select, Typography, theme } from "antd";
|
||||
|
||||
import { type ACMEDns01Provider, acmeDns01ProvidersMap } from "@/domain/provider";
|
||||
|
||||
export interface ACMEDns01ProviderSelectProps
|
||||
extends Omit<SelectProps, "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"> {
|
||||
onFilter?: (value: string, option: ACMEDns01Provider) => boolean;
|
||||
}
|
||||
import { type SharedSelectProps } from "./_shared";
|
||||
|
||||
export interface ACMEDns01ProviderSelectProps extends SharedSelectProps<ACMEDns01Provider> {}
|
||||
|
||||
const ACMEDns01ProviderSelect = ({ onFilter, ...props }: ACMEDns01ProviderSelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,37 +1,31 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMount, useSize } from "ahooks";
|
||||
import { useMount } from "ahooks";
|
||||
import { Avatar, Card, Empty, Input, type InputRef, Tag, Tooltip, Typography } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider";
|
||||
import { mergeCls } from "@/utils/css";
|
||||
|
||||
export interface AccessProviderPickerProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
gap?: number | "small" | "middle" | "large";
|
||||
placeholder?: string;
|
||||
import { type SharedPickerProps, usePickerWrapperCols } from "./_shared";
|
||||
|
||||
export interface AccessProviderPickerProps extends SharedPickerProps<AccessProvider> {
|
||||
showOptionTags?: boolean | { [key in AccessUsageType]?: boolean };
|
||||
onFilter?: (value: string, option: AccessProvider) => boolean;
|
||||
onSelect?: (value: string) => void;
|
||||
}
|
||||
|
||||
const AccessProviderPicker = ({ className, style, autoFocus, gap = "middle", placeholder, showOptionTags, onFilter, onSelect }: AccessProviderPickerProps) => {
|
||||
const AccessProviderPicker = ({
|
||||
className,
|
||||
style,
|
||||
autoFocus,
|
||||
gap = "middle",
|
||||
placeholder,
|
||||
showOptionTags,
|
||||
showSearch = true,
|
||||
onFilter,
|
||||
onSelect,
|
||||
}: AccessProviderPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperSize = useSize(wrapperRef);
|
||||
|
||||
const [keyword, setKeyword] = useState<string>();
|
||||
const keywordInputRef = useRef<InputRef>(null);
|
||||
useMount(() => {
|
||||
if (autoFocus) {
|
||||
setTimeout(() => keywordInputRef.current?.focus(), 1);
|
||||
}
|
||||
});
|
||||
|
||||
const showOptionTagForDNS = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags?.[ACCESS_USAGES.DNS] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
@@ -48,6 +42,16 @@ const AccessProviderPicker = ({ className, style, autoFocus, gap = "middle", pla
|
||||
return showOptionTagForDNS || showOptionTagForHosting || showOptionTagForCA || showOptionTagForNotification;
|
||||
}, [showOptionTagForDNS, showOptionTagForHosting, showOptionTagForCA, showOptionTagForNotification]);
|
||||
|
||||
const { wrapperElRef, cols } = usePickerWrapperCols(showOptionTagAnyhow ? 240 : 200);
|
||||
|
||||
const [keyword, setKeyword] = useState<string>();
|
||||
const keywordInputRef = useRef<InputRef>(null);
|
||||
useMount(() => {
|
||||
if (autoFocus) {
|
||||
setTimeout(() => keywordInputRef.current?.focus(), 1);
|
||||
}
|
||||
});
|
||||
|
||||
const providers = useMemo(() => {
|
||||
return Array.from(accessProvidersMap.values())
|
||||
.filter((provider) => {
|
||||
@@ -66,86 +70,80 @@ const AccessProviderPicker = ({ className, style, autoFocus, gap = "middle", pla
|
||||
return true;
|
||||
});
|
||||
}, [onFilter, keyword]);
|
||||
const providerCols = useMemo(() => {
|
||||
if (!wrapperSize) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const cols = Math.floor(wrapperSize.width / (showOptionTagAnyhow ? 240 : 200));
|
||||
return Math.min(9, Math.max(1, cols));
|
||||
}, [wrapperSize, showOptionTagAnyhow]);
|
||||
|
||||
const handleProviderTypeSelect = (value: string) => {
|
||||
onSelect?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={style} ref={wrapperRef}>
|
||||
<Input.Search ref={keywordInputRef} placeholder={placeholder ?? t("common.text.search")} onChange={(e) => setKeyword(e.target.value.trim())} />
|
||||
<div className={className} style={style} ref={wrapperElRef}>
|
||||
<Show when={showSearch}>
|
||||
<div className="mb-4">
|
||||
<Input.Search ref={keywordInputRef} placeholder={placeholder ?? t("common.text.search")} onChange={(e) => setKeyword(e.target.value.trim())} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div className="mt-4">
|
||||
<Show when={providers.length > 0} fallback={<Empty description={t("common.text.nodata")} image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
|
||||
<div
|
||||
className={mergeCls("grid w-full gap-2", `grid-cols-${providerCols}`, {
|
||||
"gap-4": gap === "large",
|
||||
"gap-2": gap === "middle",
|
||||
"gap-1": gap === "small",
|
||||
[`gap-${+gap || "2"}`]: typeof gap === "number",
|
||||
})}
|
||||
>
|
||||
{providers.map((provider) => {
|
||||
return (
|
||||
<div key={provider.type}>
|
||||
<Card
|
||||
className={mergeCls("w-full overflow-hidden shadow", provider.builtin ? " cursor-not-allowed" : "", showOptionTagAnyhow ? "h-32" : "h-28")}
|
||||
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
|
||||
hoverable
|
||||
onClick={() => {
|
||||
if (provider.builtin) {
|
||||
return;
|
||||
}
|
||||
<Show when={providers.length > 0} fallback={<Empty description={t("provider.text.nodata")} image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
|
||||
<div
|
||||
className={mergeCls("grid w-full gap-2", `grid-cols-${cols}`, {
|
||||
"gap-4": gap === "large",
|
||||
"gap-2": gap === "middle",
|
||||
"gap-1": gap === "small",
|
||||
[`gap-${+gap || "2"}`]: typeof gap === "number",
|
||||
})}
|
||||
>
|
||||
{providers.map((provider) => {
|
||||
return (
|
||||
<div key={provider.type}>
|
||||
<Card
|
||||
className={mergeCls("w-full overflow-hidden shadow", provider.builtin ? " cursor-not-allowed" : "", showOptionTagAnyhow ? "h-32" : "h-28")}
|
||||
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
|
||||
hoverable
|
||||
onClick={() => {
|
||||
if (provider.builtin) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleProviderTypeSelect(provider.type);
|
||||
}}
|
||||
>
|
||||
<div className="flex size-full flex-col items-center justify-center gap-3 overflow-hidden p-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<Avatar className="bg-stone-100" icon={<img src={provider.icon} />} shape="square" size={32} />
|
||||
</div>
|
||||
<div className="w-full overflow-hidden text-center">
|
||||
<div className={mergeCls("w-full truncate", { "mb-1": showOptionTagAnyhow })}>
|
||||
<Tooltip title={t(provider.name)} mouseEnterDelay={1}>
|
||||
<Typography.Text type={provider.builtin ? "secondary" : void 0}>{t(provider.name) || "\u00A0"}</Typography.Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Show when={showOptionTagAnyhow}>
|
||||
<div className="origin-top scale-80 whitespace-nowrap" style={{ marginInlineEnd: "-8px" }}>
|
||||
<Show when={provider.builtin}>
|
||||
<Tag>{t("access.props.provider.builtin")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForDNS && provider.usages.includes(ACCESS_USAGES.DNS)}>
|
||||
<Tag color="#d93f0b99">{t("access.props.provider.usage.dns")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForHosting && provider.usages.includes(ACCESS_USAGES.HOSTING)}>
|
||||
<Tag color="#0052cc99">{t("access.props.provider.usage.hosting")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForCA && provider.usages.includes(ACCESS_USAGES.CA)}>
|
||||
<Tag color="#0e8a1699">{t("access.props.provider.usage.ca")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForNotification && provider.usages.includes(ACCESS_USAGES.NOTIFICATION)}>
|
||||
<Tag color="#1d76db99">{t("access.props.provider.usage.notification")}</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
handleProviderTypeSelect(provider.type);
|
||||
}}
|
||||
>
|
||||
<div className="flex size-full flex-col items-center justify-center gap-3 overflow-hidden p-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<Avatar className="bg-stone-100" icon={<img src={provider.icon} />} shape="square" size={32} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div className="w-full overflow-hidden text-center">
|
||||
<div className={mergeCls("w-full truncate", { "mb-1": showOptionTagAnyhow })}>
|
||||
<Tooltip title={t(provider.name)} mouseEnterDelay={1}>
|
||||
<Typography.Text type={provider.builtin ? "secondary" : void 0}>{t(provider.name) || "\u00A0"}</Typography.Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Show when={showOptionTagAnyhow}>
|
||||
<div className="origin-top scale-80 whitespace-nowrap" style={{ marginInlineEnd: "-8px" }}>
|
||||
<Show when={provider.builtin}>
|
||||
<Tag>{t("access.props.provider.builtin")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForDNS && provider.usages.includes(ACCESS_USAGES.DNS)}>
|
||||
<Tag color="#d93f0b99">{t("access.props.provider.usage.dns")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForHosting && provider.usages.includes(ACCESS_USAGES.HOSTING)}>
|
||||
<Tag color="#0052cc99">{t("access.props.provider.usage.hosting")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForCA && provider.usages.includes(ACCESS_USAGES.CA)}>
|
||||
<Tag color="#0e8a1699">{t("access.props.provider.usage.ca")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForNotification && provider.usages.includes(ACCESS_USAGES.NOTIFICATION)}>
|
||||
<Tag color="#1d76db99">{t("access.props.provider.usage.notification")}</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar, Select, type SelectProps, Tag, Typography, theme } from "antd";
|
||||
import { Avatar, Select, Tag, Typography, theme } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider";
|
||||
|
||||
export interface AccessProviderSelectProps
|
||||
extends Omit<SelectProps, "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"> {
|
||||
import { type SharedSelectProps } from "./_shared";
|
||||
|
||||
export interface AccessProviderSelectProps extends SharedSelectProps<AccessProvider> {
|
||||
showOptionTags?: boolean | { [key in AccessUsageType]?: boolean };
|
||||
onFilter?: (value: string, option: AccessProvider) => boolean;
|
||||
}
|
||||
|
||||
const AccessProviderSelect = ({ showOptionTags, onFilter, ...props }: AccessProviderSelectProps = { showOptionTags: true }) => {
|
||||
@@ -16,6 +16,19 @@ const AccessProviderSelect = ({ showOptionTags, onFilter, ...props }: AccessProv
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const showOptionTagForDNS = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags?.[ACCESS_USAGES.DNS] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
const showOptionTagForHosting = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags?.[ACCESS_USAGES.HOSTING] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
const showOptionTagForCA = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags?.[ACCESS_USAGES.CA] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
const showOptionTagForNotification = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags?.[ACCESS_USAGES.NOTIFICATION] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
|
||||
const options = useMemo<Array<{ key: string; value: string; label: string; data: AccessProvider }>>(() => {
|
||||
return Array.from(accessProvidersMap.values())
|
||||
.filter((provider) => {
|
||||
@@ -34,19 +47,6 @@ const AccessProviderSelect = ({ showOptionTags, onFilter, ...props }: AccessProv
|
||||
}));
|
||||
}, [onFilter]);
|
||||
|
||||
const showOptionTagForDNS = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags?.[ACCESS_USAGES.DNS] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
const showOptionTagForHosting = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags?.[ACCESS_USAGES.HOSTING] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
const showOptionTagForCA = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags?.[ACCESS_USAGES.CA] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
const showOptionTagForNotification = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags?.[ACCESS_USAGES.NOTIFICATION] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
|
||||
const renderOption = (key: string) => {
|
||||
const provider = accessProvidersMap.get(key) ?? ({ type: "", name: "", icon: "", usages: [] } as unknown as AccessProvider);
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar, Select, type SelectProps, Typography, theme } from "antd";
|
||||
import { Avatar, Select, Typography, theme } from "antd";
|
||||
|
||||
import { type CAProvider, caProvidersMap } from "@/domain/provider";
|
||||
|
||||
export interface CAProviderSelectProps
|
||||
extends Omit<SelectProps, "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"> {
|
||||
onFilter?: (value: string, option: CAProvider) => boolean;
|
||||
}
|
||||
import { type SharedSelectProps } from "./_shared";
|
||||
|
||||
export interface CAProviderSelectProps extends SharedSelectProps<CAProvider> {}
|
||||
|
||||
const CAProviderSelect = ({ onFilter, ...props }: CAProviderSelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMount, useSize } from "ahooks";
|
||||
import { useMount } from "ahooks";
|
||||
import { Avatar, Card, Checkbox, Empty, Flex, Input, type InputRef, Tabs, Tooltip, Typography } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
@@ -9,24 +9,26 @@ import { useZustandShallowSelector } from "@/hooks";
|
||||
import { useAccessesStore } from "@/stores/access";
|
||||
import { mergeCls } from "@/utils/css";
|
||||
|
||||
export interface DeploymentProviderPickerProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
gap?: number | "small" | "middle" | "large";
|
||||
placeholder?: string;
|
||||
onFilter?: (value: string, option: DeploymentProvider) => boolean;
|
||||
onSelect?: (value: string) => void;
|
||||
}
|
||||
import { type SharedPickerProps, usePickerWrapperCols } from "./_shared";
|
||||
|
||||
const DeploymentProviderPicker = ({ className, style, autoFocus, gap = "middle", placeholder, onFilter, onSelect }: DeploymentProviderPickerProps) => {
|
||||
export interface DeploymentProviderPickerProps extends SharedPickerProps<DeploymentProvider> {}
|
||||
|
||||
const DeploymentProviderPicker = ({
|
||||
className,
|
||||
style,
|
||||
autoFocus,
|
||||
gap = "middle",
|
||||
placeholder,
|
||||
showSearch = true,
|
||||
onFilter,
|
||||
onSelect,
|
||||
}: DeploymentProviderPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { accesses, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "fetchAccesses"]));
|
||||
useMount(() => fetchAccesses(false));
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperSize = useSize(wrapperRef);
|
||||
const { wrapperElRef, cols } = usePickerWrapperCols(320);
|
||||
|
||||
const [isAvailableOnly, setIsAvailableOnly] = useState(true);
|
||||
|
||||
@@ -72,99 +74,91 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, gap = "middle",
|
||||
return true;
|
||||
});
|
||||
}, [onFilter, accesses, isAvailableOnly, category, keyword]);
|
||||
const providerCols = useMemo(() => {
|
||||
if (!wrapperSize) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const cols = Math.floor(wrapperSize.width / 320);
|
||||
return Math.min(9, Math.max(1, cols));
|
||||
}, [wrapperSize]);
|
||||
|
||||
const handleProviderTypeSelect = (value: string) => {
|
||||
onSelect?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={style} ref={wrapperRef}>
|
||||
<Input.Search ref={keywordInputRef} placeholder={placeholder ?? t("common.text.search")} onChange={(e) => setKeyword(e.target.value.trim())} />
|
||||
<div className={className} style={style} ref={wrapperElRef}>
|
||||
<Show when={showSearch}>
|
||||
<div className="mb-4">
|
||||
<Input.Search ref={keywordInputRef} placeholder={placeholder ?? t("common.text.search")} onChange={(e) => setKeyword(e.target.value.trim())} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div className="mt-4">
|
||||
<Flex justify="end">
|
||||
<Checkbox checked={isAvailableOnly} onClick={() => setIsAvailableOnly(!isAvailableOnly)}>
|
||||
{t("provider.text.show_available_hosting_provider_only")}
|
||||
</Checkbox>
|
||||
</Flex>
|
||||
</div>
|
||||
<Flex className="mb-4" justify="end">
|
||||
<Checkbox checked={isAvailableOnly} onClick={() => setIsAvailableOnly(!isAvailableOnly)}>
|
||||
{t("provider.text.show_available_provider_only")}
|
||||
</Checkbox>
|
||||
</Flex>
|
||||
|
||||
<div className="mt-4">
|
||||
<Flex>
|
||||
<Tabs
|
||||
defaultActiveKey={DEPLOYMENT_CATEGORIES.ALL}
|
||||
items={[
|
||||
DEPLOYMENT_CATEGORIES.ALL,
|
||||
DEPLOYMENT_CATEGORIES.CDN,
|
||||
DEPLOYMENT_CATEGORIES.STORAGE,
|
||||
DEPLOYMENT_CATEGORIES.LOADBALANCE,
|
||||
DEPLOYMENT_CATEGORIES.FIREWALL,
|
||||
DEPLOYMENT_CATEGORIES.AV,
|
||||
DEPLOYMENT_CATEGORIES.ACCELERATOR,
|
||||
DEPLOYMENT_CATEGORIES.APIGATEWAY,
|
||||
DEPLOYMENT_CATEGORIES.SERVERLESS,
|
||||
DEPLOYMENT_CATEGORIES.WEBSITE,
|
||||
DEPLOYMENT_CATEGORIES.SSL,
|
||||
DEPLOYMENT_CATEGORIES.NAS,
|
||||
DEPLOYMENT_CATEGORIES.OTHER,
|
||||
].map((key) => ({
|
||||
key: key,
|
||||
label: t(`provider.category.${key}`),
|
||||
}))}
|
||||
size="small"
|
||||
tabBarStyle={{ marginLeft: "-1rem" }}
|
||||
tabPosition="left"
|
||||
onChange={(key) => setCategory(key)}
|
||||
/>
|
||||
<Flex>
|
||||
<Tabs
|
||||
defaultActiveKey={DEPLOYMENT_CATEGORIES.ALL}
|
||||
items={[
|
||||
DEPLOYMENT_CATEGORIES.ALL,
|
||||
DEPLOYMENT_CATEGORIES.CDN,
|
||||
DEPLOYMENT_CATEGORIES.STORAGE,
|
||||
DEPLOYMENT_CATEGORIES.LOADBALANCE,
|
||||
DEPLOYMENT_CATEGORIES.FIREWALL,
|
||||
DEPLOYMENT_CATEGORIES.AV,
|
||||
DEPLOYMENT_CATEGORIES.ACCELERATOR,
|
||||
DEPLOYMENT_CATEGORIES.APIGATEWAY,
|
||||
DEPLOYMENT_CATEGORIES.SERVERLESS,
|
||||
DEPLOYMENT_CATEGORIES.WEBSITE,
|
||||
DEPLOYMENT_CATEGORIES.SSL,
|
||||
DEPLOYMENT_CATEGORIES.NAS,
|
||||
DEPLOYMENT_CATEGORIES.OTHER,
|
||||
].map((key) => ({
|
||||
key: key,
|
||||
label: t(`provider.category.${key}`),
|
||||
}))}
|
||||
size="small"
|
||||
tabBarStyle={{ marginLeft: "-1rem" }}
|
||||
tabPosition="left"
|
||||
onChange={(key) => setCategory(key)}
|
||||
/>
|
||||
|
||||
<div className="flex-1">
|
||||
<Show when={providers.length > 0} fallback={<Empty description={t("common.text.nodata")} image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
|
||||
<div
|
||||
className={mergeCls("grid w-full gap-2", `grid-cols-${providerCols}`, {
|
||||
"gap-4": gap === "large",
|
||||
"gap-2": gap === "middle",
|
||||
"gap-1": gap === "small",
|
||||
[`gap-${+gap || "2"}`]: typeof gap === "number",
|
||||
})}
|
||||
>
|
||||
{providers.map((provider) => {
|
||||
return (
|
||||
<div key={provider.type}>
|
||||
<Card
|
||||
className="h-16 w-full overflow-hidden shadow"
|
||||
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
|
||||
hoverable
|
||||
onClick={() => {
|
||||
handleProviderTypeSelect(provider.type);
|
||||
}}
|
||||
>
|
||||
<div className="flex size-full items-center gap-4 overflow-hidden">
|
||||
<Avatar className="bg-stone-100" icon={<img src={provider.icon} />} shape="square" size={28} />
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="line-clamp-2 max-w-full">
|
||||
<Tooltip title={t(provider.name)} mouseEnterDelay={1}>
|
||||
<Typography.Text>{t(provider.name) || "\u00A0"}</Typography.Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Show when={providers.length > 0} fallback={<Empty description={t("provider.text.nodata")} image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
|
||||
<div
|
||||
className={mergeCls("grid w-full gap-2", `grid-cols-${cols}`, {
|
||||
"gap-4": gap === "large",
|
||||
"gap-2": gap === "middle",
|
||||
"gap-1": gap === "small",
|
||||
[`gap-${+gap || "2"}`]: typeof gap === "number",
|
||||
})}
|
||||
>
|
||||
{providers.map((provider) => {
|
||||
return (
|
||||
<div key={provider.type}>
|
||||
<Card
|
||||
className="h-16 w-full overflow-hidden shadow"
|
||||
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
|
||||
hoverable
|
||||
onClick={() => {
|
||||
handleProviderTypeSelect(provider.type);
|
||||
}}
|
||||
>
|
||||
<div className="flex size-full items-center gap-4 overflow-hidden">
|
||||
<Avatar className="bg-stone-100" icon={<img src={provider.icon} />} shape="square" size={28} />
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="line-clamp-2 max-w-full">
|
||||
<Tooltip title={t(provider.name)} mouseEnterDelay={1}>
|
||||
<Typography.Text>{t(provider.name) || "\u00A0"}</Typography.Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar, Select, type SelectProps, Typography, theme } from "antd";
|
||||
import { Avatar, Select, Typography, theme } from "antd";
|
||||
|
||||
import { type DeploymentProvider, deploymentProvidersMap } from "@/domain/provider";
|
||||
|
||||
export interface DeploymentProviderSelectProps
|
||||
extends Omit<SelectProps, "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"> {
|
||||
onFilter?: (value: string, option: DeploymentProvider) => boolean;
|
||||
}
|
||||
import { type SharedSelectProps } from "./_shared";
|
||||
|
||||
export interface DeploymentProviderSelectProps extends SharedSelectProps<DeploymentProvider> {}
|
||||
|
||||
const DeploymentProviderSelect = ({ onFilter, ...props }: DeploymentProviderSelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMount } from "ahooks";
|
||||
import { Avatar, Card, Checkbox, Empty, Flex, Input, type InputRef, Tooltip, Typography } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { type NotificationProvider, notificationProvidersMap } from "@/domain/provider";
|
||||
import { useZustandShallowSelector } from "@/hooks";
|
||||
import { useAccessesStore } from "@/stores/access";
|
||||
import { mergeCls } from "@/utils/css";
|
||||
|
||||
import { type SharedPickerProps, usePickerWrapperCols } from "./_shared";
|
||||
|
||||
export interface NotificationProviderPickerProps extends SharedPickerProps<NotificationProvider> {}
|
||||
|
||||
const NotificationProviderPicker = ({
|
||||
className,
|
||||
style,
|
||||
autoFocus,
|
||||
gap = "middle",
|
||||
placeholder,
|
||||
showSearch = true,
|
||||
onFilter,
|
||||
onSelect,
|
||||
}: NotificationProviderPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { accesses, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "fetchAccesses"]));
|
||||
useMount(() => fetchAccesses(false));
|
||||
|
||||
const { wrapperElRef, cols } = usePickerWrapperCols(320);
|
||||
|
||||
const [isAvailableOnly, setIsAvailableOnly] = useState(true);
|
||||
|
||||
const [keyword, setKeyword] = useState<string>();
|
||||
const keywordInputRef = useRef<InputRef>(null);
|
||||
useMount(() => {
|
||||
if (autoFocus) {
|
||||
setTimeout(() => keywordInputRef.current?.focus(), 1);
|
||||
}
|
||||
});
|
||||
|
||||
const providers = useMemo(() => {
|
||||
return Array.from(notificationProvidersMap.values())
|
||||
.filter((provider) => {
|
||||
if (onFilter) {
|
||||
return onFilter(provider.type, provider);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.filter((provider) => {
|
||||
if (isAvailableOnly) {
|
||||
return accesses.some((access) => access.provider === provider.provider);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.filter((provider) => {
|
||||
if (keyword) {
|
||||
const value = keyword.toLowerCase();
|
||||
return provider.type.toLowerCase().includes(value) || t(provider.name).toLowerCase().includes(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [onFilter, accesses, isAvailableOnly, keyword]);
|
||||
|
||||
const handleProviderTypeSelect = (value: string) => {
|
||||
onSelect?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={style} ref={wrapperElRef}>
|
||||
<Show when={showSearch}>
|
||||
<div className="mb-4">
|
||||
<Input.Search ref={keywordInputRef} placeholder={placeholder ?? t("common.text.search")} onChange={(e) => setKeyword(e.target.value.trim())} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Flex className="mb-4" justify="end">
|
||||
<Checkbox checked={isAvailableOnly} onClick={() => setIsAvailableOnly(!isAvailableOnly)}>
|
||||
{t("provider.text.show_available_provider_only")}
|
||||
</Checkbox>
|
||||
</Flex>
|
||||
|
||||
<Show when={providers.length > 0} fallback={<Empty description={t("provider.text.nodata")} image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
|
||||
<div
|
||||
className={mergeCls("grid w-full gap-2", `grid-cols-${cols}`, {
|
||||
"gap-4": gap === "large",
|
||||
"gap-2": gap === "middle",
|
||||
"gap-1": gap === "small",
|
||||
[`gap-${+gap || "2"}`]: typeof gap === "number",
|
||||
})}
|
||||
>
|
||||
{providers.map((provider) => {
|
||||
return (
|
||||
<div key={provider.type}>
|
||||
<Card
|
||||
className="h-16 w-full overflow-hidden shadow"
|
||||
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
|
||||
hoverable
|
||||
onClick={() => {
|
||||
handleProviderTypeSelect(provider.type);
|
||||
}}
|
||||
>
|
||||
<div className="flex size-full items-center gap-4 overflow-hidden">
|
||||
<Avatar className="bg-stone-100" icon={<img src={provider.icon} />} shape="square" size={28} />
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="line-clamp-2 max-w-full">
|
||||
<Tooltip title={t(provider.name)} mouseEnterDelay={1}>
|
||||
<Typography.Text>{t(provider.name) || "\u00A0"}</Typography.Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationProviderPicker;
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar, Select, type SelectProps, Typography, theme } from "antd";
|
||||
import { Avatar, Select, Typography, theme } from "antd";
|
||||
|
||||
import { type NotificationProvider, notificationProvidersMap } from "@/domain/provider";
|
||||
|
||||
export interface NotificationProviderSelectProps
|
||||
extends Omit<SelectProps, "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"> {
|
||||
onFilter?: (value: string, option: NotificationProvider) => boolean;
|
||||
}
|
||||
import { type SharedSelectProps } from "./_shared";
|
||||
|
||||
export interface NotificationProviderSelectProps extends SharedSelectProps<NotificationProvider> {}
|
||||
|
||||
const NotificationProviderSelect = ({ onFilter, ...props }: NotificationProviderSelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useSize } from "ahooks";
|
||||
import { type SelectProps } from "antd";
|
||||
|
||||
type Provider = { type: string; name: string };
|
||||
|
||||
export interface SharedSelectProps<T extends Provider>
|
||||
extends Omit<SelectProps, "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"> {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onFilter?: (value: string, option: T) => boolean;
|
||||
}
|
||||
|
||||
export interface SharedPickerProps<T extends Provider> {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
gap?: number | "small" | "middle" | "large";
|
||||
placeholder?: string;
|
||||
showSearch?: boolean;
|
||||
onFilter?: (value: string, option: T) => boolean;
|
||||
onSelect?: (value: string) => void;
|
||||
}
|
||||
|
||||
export const usePickerWrapperCols = (width: number) => {
|
||||
const wrapperElRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperSize = useSize(wrapperElRef);
|
||||
|
||||
const cols = useMemo(() => {
|
||||
if (!wrapperSize) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const cols = Math.floor(wrapperSize.width / width);
|
||||
return Math.min(9, Math.max(1, cols));
|
||||
}, [wrapperSize, width]);
|
||||
|
||||
return {
|
||||
wrapperElRef,
|
||||
cols,
|
||||
};
|
||||
};
|
||||
@@ -28,7 +28,7 @@ const BizDeployNodeConfigDrawer = ({ node, ...props }: BizDeployNodeConfigDrawer
|
||||
return (
|
||||
<NodeConfigDrawer
|
||||
anchor={fieldProvider ? { items: BizDeployNodeConfigForm.getAnchorItems({ i18n }) } : false}
|
||||
footer={fieldProvider ? true : false}
|
||||
footer={fieldProvider ? void 0 : false}
|
||||
form={formInst}
|
||||
node={node}
|
||||
{...props}
|
||||
|
||||
@@ -467,7 +467,7 @@ const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProp
|
||||
const handleProviderSelect = (value?: string | undefined) => {
|
||||
// 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标
|
||||
if (initialValues?.provider === value) {
|
||||
formInst.resetFields();
|
||||
formInst.resetFields(["providerConfig"]);
|
||||
} else {
|
||||
const oldValues = formInst.getFieldsValue();
|
||||
const newValues: Record<string, unknown> = {};
|
||||
@@ -537,10 +537,7 @@ const BizDeployNodeConfigForm = ({ node, ...props }: BizDeployNodeConfigFormProp
|
||||
showSearch
|
||||
onFilter={(_, option) => {
|
||||
if (option.reserve) return false;
|
||||
if (fieldProvider) return deploymentProvidersMap.get(fieldProvider)?.provider === option.provider;
|
||||
|
||||
const provider = accessProvidersMap.get(option.provider);
|
||||
return !!provider?.usages?.includes(ACCESS_USAGES.HOSTING);
|
||||
return deploymentProvidersMap.get(fieldProvider)?.provider === option.provider;
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -23,11 +23,12 @@ const BizNotifyNodeConfigDrawer = ({ node, ...props }: BizNotifyNodeConfigDrawer
|
||||
|
||||
const [formInst] = Form.useForm();
|
||||
|
||||
const fieldProvider = Form.useWatch<string>("provider", { form: formInst, preserve: true });
|
||||
|
||||
return (
|
||||
<NodeConfigDrawer
|
||||
anchor={{
|
||||
items: BizNotifyNodeConfigForm.getAnchorItems({ i18n }),
|
||||
}}
|
||||
anchor={fieldProvider ? { items: BizNotifyNodeConfigForm.getAnchorItems({ i18n }) } : false}
|
||||
footer={fieldProvider ? void 0 : false}
|
||||
form={formInst}
|
||||
node={node}
|
||||
{...props}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { getI18n, useTranslation } from "react-i18next";
|
||||
import { type FlowNodeEntity, getNodeForm } from "@flowgram.ai/fixed-layout-editor";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
@@ -8,7 +8,9 @@ import { z } from "zod";
|
||||
|
||||
import AccessEditDrawer from "@/components/access/AccessEditDrawer";
|
||||
import AccessSelect from "@/components/access/AccessSelect";
|
||||
import NotificationProviderPicker from "@/components/provider/NotificationProviderPicker";
|
||||
import NotificationProviderSelect from "@/components/provider/NotificationProviderSelect";
|
||||
import Show from "@/components/Show";
|
||||
import { ACCESS_USAGES, NOTIFICATION_PROVIDERS, accessProvidersMap, notificationProvidersMap } from "@/domain/provider";
|
||||
import { type WorkflowNodeConfigForBizNotify, defaultNodeConfigForBizNotify } from "@/domain/workflow";
|
||||
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
|
||||
@@ -79,134 +81,113 @@ const BizNotifyNodeConfigForm = ({ node, ...props }: BizNotifyNodeConfigFormProp
|
||||
}
|
||||
}, [fieldProvider]);
|
||||
|
||||
const [showProvider, setShowProvider] = useState(false);
|
||||
useEffect(() => {
|
||||
// 通常情况下每个授权信息只对应一个消息通知提供商,此时无需显示消息通知提供商字段;
|
||||
// 如果对应多个,则显示。
|
||||
if (fieldProviderAccessId) {
|
||||
const access = accesses.find((e) => e.id === fieldProviderAccessId);
|
||||
const providers = Array.from(notificationProvidersMap.values()).filter((e) => e.provider === access?.provider);
|
||||
setShowProvider(providers.length > 1);
|
||||
} else {
|
||||
setShowProvider(false);
|
||||
}
|
||||
}, [accesses, fieldProviderAccessId]);
|
||||
|
||||
const handleProviderSelect = (value: string) => {
|
||||
// 切换消息通知提供商时联动授权信息
|
||||
if (initialValues?.provider === value) {
|
||||
formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId);
|
||||
} else {
|
||||
if (notificationProvidersMap.get(fieldProvider)?.provider !== notificationProvidersMap.get(value)?.provider) {
|
||||
formInst.setFieldValue("providerAccessId", void 0);
|
||||
}
|
||||
}
|
||||
const handleProviderPick = (value: string) => {
|
||||
formInst.setFieldValue("provider", value);
|
||||
};
|
||||
|
||||
const handleProviderAccessSelect = (value: string) => {
|
||||
// 切换授权信息时联动消息通知提供商
|
||||
const access = accesses.find((access) => access.id === value);
|
||||
const provider = Array.from(notificationProvidersMap.values()).find((provider) => provider.provider === access?.provider);
|
||||
if (fieldProvider !== provider?.type) {
|
||||
formInst.setFieldValue("provider", provider?.type);
|
||||
const handleProviderSelect = (value?: string | undefined) => {
|
||||
// 切换通知渠道时重置表单,避免其他通知渠道的配置字段影响当前通知渠道
|
||||
if (initialValues?.provider === value) {
|
||||
formInst.resetFields(["providerConfig"]);
|
||||
} else {
|
||||
if (notificationProvidersMap.get(fieldProvider)?.provider !== notificationProvidersMap.get(value!)?.provider) {
|
||||
formInst.setFieldValue("providerAccessId", void 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeFormContextProvider value={{ node }}>
|
||||
<Form {...formProps} clearOnDestroy={true} form={formInst} layout="vertical" preserve={false} scrollToFirstError>
|
||||
<div id="parameters" data-anchor="parameters">
|
||||
<Form.Item name="subject" label={t("workflow_node.notify.form.subject.label")} rules={[formRule]}>
|
||||
<Input placeholder={t("workflow_node.notify.form.subject.placeholder")} />
|
||||
</Form.Item>
|
||||
<Show when={!fieldProvider}>
|
||||
<NotificationProviderPicker autoFocus placeholder={t("workflow_node.notify.form.provider.search.placeholder")} onSelect={handleProviderPick} />
|
||||
</Show>
|
||||
|
||||
<Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
|
||||
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ display: fieldProvider ? "block" : "none" }}>
|
||||
<div id="parameters" data-anchor="parameters">
|
||||
<Form.Item name="subject" label={t("workflow_node.notify.form.subject.label")} rules={[formRule]}>
|
||||
<Input placeholder={t("workflow_node.notify.form.subject.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<div id="channel" data-anchor="channel">
|
||||
<Divider size="small">
|
||||
<Typography.Text className="text-xs font-normal" type="secondary">
|
||||
{t("workflow_node.notify.form_anchor.channel.title")}
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
<Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
|
||||
<Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item name="provider" label={t("workflow_node.notify.form.provider.label")} hidden={!showProvider} rules={[formRule]}>
|
||||
<NotificationProviderSelect
|
||||
disabled={!showProvider}
|
||||
placeholder={t("workflow_node.notify.form.provider.placeholder")}
|
||||
showSearch
|
||||
onFilter={(_, option) => {
|
||||
if (fieldProviderAccessId) {
|
||||
return accesses.find((e) => e.id === fieldProviderAccessId)?.provider === option.provider;
|
||||
}
|
||||
<div id="channel" data-anchor="channel">
|
||||
<Divider size="small">
|
||||
<Typography.Text className="text-xs font-normal" type="secondary">
|
||||
{t("workflow_node.notify.form_anchor.channel.title")}
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
|
||||
return true;
|
||||
}}
|
||||
onSelect={handleProviderSelect}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("workflow_node.notify.form.provider_access.label")}>
|
||||
<div className="absolute -top-[6px] right-0 -translate-y-full">
|
||||
<AccessEditDrawer
|
||||
mode="create"
|
||||
trigger={
|
||||
<Button size="small" type="link">
|
||||
{t("workflow_node.notify.form.provider_access.button")}
|
||||
<IconPlus size="1.25em" />
|
||||
</Button>
|
||||
}
|
||||
usage="notification"
|
||||
afterSubmit={(record) => {
|
||||
const provider = accessProvidersMap.get(record.provider);
|
||||
if (provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION)) {
|
||||
formInst.setFieldValue("providerAccessId", record.id);
|
||||
handleProviderAccessSelect(record.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Item name="providerAccessId" noStyle rules={[formRule]}>
|
||||
<AccessSelect
|
||||
placeholder={t("workflow_node.notify.form.provider_access.placeholder")}
|
||||
<Form.Item name="provider" label={t("workflow_node.notify.form.provider.label")} rules={[formRule]}>
|
||||
<NotificationProviderSelect
|
||||
allowClear
|
||||
disabled={!!initialValues?.provider}
|
||||
placeholder={t("workflow_node.notify.form.provider.placeholder")}
|
||||
showSearch
|
||||
onChange={handleProviderAccessSelect}
|
||||
onFilter={(_, option) => {
|
||||
if (option.reserve !== "notif") return false;
|
||||
|
||||
const provider = accessProvidersMap.get(option.provider);
|
||||
return !!provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION);
|
||||
}}
|
||||
onSelect={handleProviderSelect}
|
||||
onClear={handleProviderSelect}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
<FormNestedFieldsContextProvider value={{ parentNamePath: "providerConfig" }}>
|
||||
{NestedProviderConfigFields && <NestedProviderConfigFields />}
|
||||
</FormNestedFieldsContextProvider>
|
||||
</div>
|
||||
|
||||
<div id="strategy" data-anchor="strategy">
|
||||
<Divider size="small">
|
||||
<Typography.Text className="text-xs font-normal" type="secondary">
|
||||
{t("workflow_node.notify.form_anchor.strategy.title")}
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item label={t("workflow_node.notify.form.skip_on_all_prev_skipped.label")}>
|
||||
<Flex align="center" gap={8} wrap="wrap">
|
||||
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.prefix")}</div>
|
||||
<Form.Item name="skipOnAllPrevSkipped" noStyle rules={[formRule]}>
|
||||
<Switch
|
||||
checkedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.on")}
|
||||
unCheckedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.off")}
|
||||
<Form.Item label={t("workflow_node.notify.form.provider_access.label")}>
|
||||
<div className="absolute -top-[6px] right-0 -translate-y-full">
|
||||
<AccessEditDrawer
|
||||
mode="create"
|
||||
trigger={
|
||||
<Button size="small" type="link">
|
||||
{t("workflow_node.notify.form.provider_access.button")}
|
||||
<IconPlus size="1.25em" />
|
||||
</Button>
|
||||
}
|
||||
usage="notification"
|
||||
afterSubmit={(record) => {
|
||||
const provider = accessProvidersMap.get(record.provider);
|
||||
if (provider?.usages?.includes(ACCESS_USAGES.NOTIFICATION)) {
|
||||
formInst.setFieldValue("providerAccessId", record.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Form.Item name="providerAccessId" noStyle rules={[formRule]}>
|
||||
<AccessSelect
|
||||
placeholder={t("workflow_node.notify.form.provider_access.placeholder")}
|
||||
showSearch
|
||||
onFilter={(_, option) => {
|
||||
if (option.reserve !== "notif") return false;
|
||||
return notificationProvidersMap.get(fieldProvider)?.provider === option.provider;
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.suffix")}</div>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
<FormNestedFieldsContextProvider value={{ parentNamePath: "providerConfig" }}>
|
||||
{NestedProviderConfigFields && <NestedProviderConfigFields />}
|
||||
</FormNestedFieldsContextProvider>
|
||||
</div>
|
||||
|
||||
<div id="strategy" data-anchor="strategy">
|
||||
<Divider size="small">
|
||||
<Typography.Text className="text-xs font-normal" type="secondary">
|
||||
{t("workflow_node.notify.form_anchor.strategy.title")}
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item label={t("workflow_node.notify.form.skip_on_all_prev_skipped.label")}>
|
||||
<Flex align="center" gap={8} wrap="wrap">
|
||||
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.prefix")}</div>
|
||||
<Form.Item name="skipOnAllPrevSkipped" noStyle rules={[formRule]}>
|
||||
<Switch
|
||||
checkedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.on")}
|
||||
unCheckedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.off")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.suffix")}</div>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</NodeFormContextProvider>
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"provider.category.nas": "NAS",
|
||||
"provider.category.other": "Other",
|
||||
|
||||
"provider.text.nodata": "No providers available",
|
||||
"provider.text.default_ca_provider": "(Default) Follow global settings",
|
||||
"provider.text.show_available_hosting_provider_only": "Only display available providers (with added credentials)"
|
||||
"provider.text.show_available_provider_only": "Only display available providers (with added credentials)"
|
||||
}
|
||||
|
||||
@@ -147,9 +147,9 @@
|
||||
"workflow_node.deploy.form.certificate_output_node_id.label": "Certificate to deploy",
|
||||
"workflow_node.deploy.form.certificate_output_node_id.placeholder": "Please select certificate to deploy",
|
||||
"workflow_node.deploy.form.certificate_output_node_id.help": "Notes: The certificate to be deployed comes from the previous nodes of application or upload.",
|
||||
"workflow_node.deploy.form.provider.label": "Deploy target",
|
||||
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
|
||||
"workflow_node.deploy.form.provider.search.placeholder": "Search deploy target ...",
|
||||
"workflow_node.deploy.form.provider.label": "Deployment target",
|
||||
"workflow_node.deploy.form.provider.placeholder": "Please select deployment target",
|
||||
"workflow_node.deploy.form.provider.search.placeholder": "Search deployment target ...",
|
||||
"workflow_node.deploy.form.provider_access.label": "Hosting provider credential",
|
||||
"workflow_node.deploy.form.provider_access.placeholder": "Please select an credential of Hosting provider",
|
||||
"workflow_node.deploy.form.provider_access.tooltip": "Used to invoke API during deployment.",
|
||||
@@ -997,6 +997,7 @@
|
||||
"workflow_node.notify.form.message.placeholder": "Please enter message",
|
||||
"workflow_node.notify.form.provider.label": "Notification channel",
|
||||
"workflow_node.notify.form.provider.placeholder": "Please select notification channel",
|
||||
"workflow_node.notify.form.provider.search.placeholder": "Search notification channel ...",
|
||||
"workflow_node.notify.form.provider_access.label": "Notification provider credential",
|
||||
"workflow_node.notify.form.provider_access.placeholder": "Please select an credential of notification provider",
|
||||
"workflow_node.notify.form.provider_access.button": "Create",
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
"provider.category.nas": "NAS",
|
||||
"provider.category.other": "其他",
|
||||
|
||||
"provider.text.nodata": "暂无提供商",
|
||||
"provider.text.default_ca_provider": "(默认)不指定,跟随全局设置",
|
||||
"provider.text.show_available_hosting_provider_only": "仅列出可用的提供商(即已添加过授权凭据的)"
|
||||
"provider.text.show_available_provider_only": "仅列出可用的提供商(即已添加过授权凭据的)"
|
||||
}
|
||||
|
||||
@@ -995,6 +995,7 @@
|
||||
"workflow_node.notify.form.message.placeholder": "请输入通知内容",
|
||||
"workflow_node.notify.form.provider.label": "通知渠道",
|
||||
"workflow_node.notify.form.provider.placeholder": "请选择通知渠道",
|
||||
"workflow_node.notify.form.provider.search.placeholder": "搜索通知渠道……",
|
||||
"workflow_node.notify.form.provider_access.label": "通知渠道授权",
|
||||
"workflow_node.notify.form.provider_access.placeholder": "请选择通知渠道授权",
|
||||
"workflow_node.notify.form.provider_access.button": "新建",
|
||||
|
||||
Reference in New Issue
Block a user