refactor(action): replace ActionCard with RuleCard and restructure action settings
- Removed ActionCard component and its associated imports. - Introduced RuleCard component to handle action rules. - Updated ActionSetting to utilize RuleCard instead of ActionCard. - Added new sections for handling conditions and actions (WhenSection, ThenSection). - Introduced constants for available actions and their configurations. - Removed TargetActionList component as part of the refactor. This refactor enhances the modularity and maintainability of the action settings UI. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
95b23c3be7
commit
1ace5eaaa3
|
|
@ -1,72 +0,0 @@
|
|||
import { Card, CardContent, CardHeader } from "@follow/components/ui/card/index.jsx"
|
||||
import { Input } from "@follow/components/ui/input/index.js"
|
||||
import { Switch } from "@follow/components/ui/switch/index.jsx"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.js"
|
||||
import { useActionRule } from "@follow/store/action/hooks"
|
||||
import { actionActions } from "@follow/store/action/store"
|
||||
import { clsx } from "@follow/utils/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { FeedFilter } from "./feed-filter"
|
||||
import { TargetActionList } from "./target-action-list"
|
||||
|
||||
export const ActionCard = ({ index }: { index: number }) => {
|
||||
const { t } = useTranslation("common")
|
||||
return (
|
||||
<Card className="group relative">
|
||||
<CardHeader>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
type="button"
|
||||
className={clsx(
|
||||
"center bg-background ring-border flex size-6 rounded-full p-1 shadow-sm ring-1",
|
||||
"absolute -right-2 -top-2 z-[1] opacity-100 duration-200 hover:!opacity-100 group-hover:opacity-70 lg:opacity-0",
|
||||
)}
|
||||
onClick={() => {
|
||||
actionActions.deleteRule(index)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-close-cute-re text-lg" />
|
||||
<span className="sr-only">{t("words.delete")}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("words.delete")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<ActionCardToolbar index={index} />
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap justify-between gap-y-6">
|
||||
<FeedFilter index={index} />
|
||||
<TargetActionList index={index} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const ActionCardToolbar = ({ index }: { index: number }) => {
|
||||
const { t } = useTranslation("settings")
|
||||
|
||||
const name = useActionRule(index, (a) => a.name)
|
||||
const disabled = useActionRule(index, (a) => a.result.disabled)
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<p className="shrink-0 font-medium text-zinc-500">{t("actions.action_card.name")}</p>
|
||||
<Input
|
||||
value={name}
|
||||
className="h-8 max-w-64"
|
||||
onChange={(e) => {
|
||||
actionActions.patchRule(index, { name: e.target.value })
|
||||
}}
|
||||
/>
|
||||
<div className="grow" />
|
||||
|
||||
<Switch
|
||||
checked={!disabled}
|
||||
onCheckedChange={(checked) => {
|
||||
actionActions.patchRule(index, {
|
||||
result: { disabled: !checked },
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -7,36 +7,39 @@ import {
|
|||
useUpdateActionsMutation,
|
||||
} from "@follow/store/action/hooks"
|
||||
import { actionActions } from "@follow/store/action/store"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { unstable_usePrompt } from "react-router"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { queryClient } from "~/lib/query-client"
|
||||
import { ActionCard } from "~/modules/action/action-card"
|
||||
import { RuleCard } from "~/modules/action/rule-card"
|
||||
|
||||
export const ActionSetting = () => {
|
||||
const actions = useActionRules()
|
||||
|
||||
const actionQuery = usePrefetchActions()
|
||||
const actionLength = useActionRules((actions) => actions.length)
|
||||
|
||||
if (actionQuery.isPending) {
|
||||
return <LoadingWithIcon icon={<i className="i-mgc-magic-2-cute-re" />} size="large" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl space-y-4">
|
||||
{Array.from({ length: actionLength }).map((_action, actionIdx) => (
|
||||
<ActionCard key={actionIdx} index={actionIdx} />
|
||||
))}
|
||||
<ActionSettingOperations />
|
||||
<div className="flex w-full max-w-5xl flex-col gap-4 p-2">
|
||||
<ActionButtonGroup />
|
||||
<div className="@container flex flex-col gap-8">
|
||||
{actions.map((_, actionIdx) => {
|
||||
return <RuleCard key={actionIdx} index={actionIdx} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ActionSettingOperations() {
|
||||
const { t } = useTranslation("settings")
|
||||
|
||||
const ActionButtonGroup = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const actionLength = useActionRules((actions) => actions.length)
|
||||
const isDirty = useIsActionDataDirty()
|
||||
const { t } = useTranslation("settings")
|
||||
unstable_usePrompt({
|
||||
message: t("actions.navigate.prompt"),
|
||||
when: ({ currentLocation, nextLocation }) =>
|
||||
|
|
@ -57,23 +60,17 @@ function ActionSettingOperations() {
|
|||
})
|
||||
|
||||
return (
|
||||
<div className="flex justify-end gap-x-2">
|
||||
<div className="flex w-full items-center justify-end gap-2">
|
||||
<Button
|
||||
variant={actionLength > 0 ? "outline" : "primary"}
|
||||
onClick={() => {
|
||||
actionActions.addRule((number) => t("actions.actionName", { number }))
|
||||
}}
|
||||
variant={actionLength === 0 ? "primary" : "outline"}
|
||||
onClick={() => actionActions.addRule((number) => t("actions.actionName", { number }))}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re mr-1" />
|
||||
<span>{t("actions.newRule")}</span>
|
||||
<i className="i-mingcute-add-line mr-2" />
|
||||
{t("actions.newRule")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!isDirty}
|
||||
isLoading={mutation.isPending}
|
||||
onClick={() => mutation.mutate()}
|
||||
>
|
||||
<i className="i-mgc-check-circle-cute-re mr-1" />
|
||||
|
||||
<Button onClick={() => mutation.mutate()}>
|
||||
<i className="i-mgc-check-circle-cute-re mr-2" />
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import { ResponsiveSelect } from "@follow/components/ui/select/responsive.js"
|
||||
import { ACTION_LANGUAGE_MAP } from "@follow/shared/language"
|
||||
import { availableActionMap as availableActionMapOriginal } from "@follow/store/action/constant"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { defaultResources } from "~/@types/default-resource"
|
||||
import {
|
||||
DEFAULT_ACTION_LANGUAGE,
|
||||
setGeneralSetting,
|
||||
useGeneralSettingKey,
|
||||
} from "~/atoms/settings/general"
|
||||
|
||||
import { setTranslationCache } from "../entry-content/atoms"
|
||||
|
||||
export const availableActionMap: typeof availableActionMapOriginal = {
|
||||
...availableActionMapOriginal,
|
||||
summary: {
|
||||
...availableActionMapOriginal.summary,
|
||||
prefixElement: <AiTargetLanguageSelector />,
|
||||
},
|
||||
translation: {
|
||||
...availableActionMapOriginal.translation,
|
||||
prefixElement: <AiTargetLanguageSelector />,
|
||||
},
|
||||
}
|
||||
|
||||
function AiTargetLanguageSelector() {
|
||||
const { t } = useTranslation("settings")
|
||||
const actionLanguage = useGeneralSettingKey("actionLanguage")
|
||||
|
||||
return (
|
||||
<ResponsiveSelect
|
||||
size="sm"
|
||||
triggerClassName="w-48"
|
||||
defaultValue={actionLanguage}
|
||||
value={actionLanguage}
|
||||
onValueChange={(value) => {
|
||||
setGeneralSetting("actionLanguage", value)
|
||||
setTranslationCache({})
|
||||
}}
|
||||
items={[
|
||||
{ label: t("general.action_language.default"), value: DEFAULT_ACTION_LANGUAGE },
|
||||
...Object.values(ACTION_LANGUAGE_MAP).map((item) => ({
|
||||
label: defaultResources[item.value].lang.name,
|
||||
value: item.value,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { Input } from "@follow/components/ui/input/index.js"
|
||||
import { Switch } from "@follow/components/ui/switch/index.jsx"
|
||||
import { useActionRule } from "@follow/store/action/hooks"
|
||||
import { actionActions } from "@follow/store/action/store"
|
||||
|
||||
import { ThenSection } from "./then-section"
|
||||
import { WhenSection } from "./when-section"
|
||||
|
||||
export const RuleCard = ({ index }: { index: number }) => {
|
||||
return (
|
||||
<div className="bg-material-ultra-thin group/card-root relative flex flex-col gap-4 rounded-2xl border p-4 shadow-sm md:p-6">
|
||||
<RuleCardToolbar index={index} />
|
||||
<div className="@[800px]:grid-cols-2 grid grid-cols-1 gap-x-8 gap-y-6">
|
||||
<WhenSection index={index} />
|
||||
<ThenSection index={index} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RuleCardToolbar = ({ index }: { index: number }) => {
|
||||
const name = useActionRule(index, (a) => a.name)
|
||||
const disabled = useActionRule(index, (a) => a.result.disabled)
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-3">
|
||||
<Input
|
||||
value={name}
|
||||
className="h-9 max-w-64 bg-transparent px-2 text-lg font-semibold shadow-none ring-0 focus-visible:ring-0"
|
||||
onChange={(e) => {
|
||||
actionActions.patchRule(index, { name: e.target.value })
|
||||
}}
|
||||
/>
|
||||
<div className="grow" />
|
||||
|
||||
<Switch
|
||||
checked={!disabled}
|
||||
onCheckedChange={(checked) => {
|
||||
actionActions.patchRule(index, {
|
||||
result: { disabled: !checked },
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-0 top-0 opacity-0 transition-opacity duration-200 group-hover/card-root:opacity-100">
|
||||
<button
|
||||
className="bg-background center flex size-8 -translate-y-1/2 translate-x-1/2 rounded-full border"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
actionActions.deleteRule(index)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-close-cute-re" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
import { ActionButton, Button } from "@follow/components/ui/button/index.js"
|
||||
import { Divider } from "@follow/components/ui/divider/index.js"
|
||||
import { Input } from "@follow/components/ui/input/index.js"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@follow/components/ui/table/index.jsx"
|
||||
import type { ActionId } from "@follow/models/types"
|
||||
import type { ActionAction } from "@follow/store/action/constant"
|
||||
import { availableActionMap } from "@follow/store/action/constant"
|
||||
import { useActionRule } from "@follow/store/action/hooks"
|
||||
import { actionActions } from "@follow/store/action/store"
|
||||
import { merge } from "es-toolkit/compat"
|
||||
import { Fragment, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu/dropdown-menu"
|
||||
|
||||
import { useSettingModal } from "../settings/modal/use-setting-modal"
|
||||
|
||||
const AddTableRow = ({ onClick, disabled }: { onClick?: () => void; disabled?: boolean }) => {
|
||||
const { t } = useTranslation("settings")
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
textClassName="w-full"
|
||||
buttonClassName="py-1 mt-1 gap-1 w-full"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t("actions.action_card.add")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const DeleteTableCell = ({ disabled, onClick }: { disabled?: boolean; onClick?: () => void }) => (
|
||||
<TableCell size="sm" className="flex h-10 items-center">
|
||||
<ActionButton disabled={disabled} onClick={onClick}>
|
||||
<i className="i-mgc-delete-2-cute-re text-zinc-600" />
|
||||
</ActionButton>
|
||||
</TableCell>
|
||||
)
|
||||
|
||||
export const TargetActionList = ({ index }: { index: number }) => {
|
||||
const result = useActionRule(index, (a) => a.result)
|
||||
|
||||
const rewriteRules = useActionRule(index, (a) => a.result.rewriteRules)
|
||||
const webhooks = useActionRule(index, (a) => a.result.webhooks)
|
||||
const settingModalPresent = useSettingModal()
|
||||
|
||||
const disabled = useActionRule(index, (a) => a.result.disabled)
|
||||
const { t } = useTranslation("settings")
|
||||
|
||||
const availableActions = useMemo(() => {
|
||||
const extendedAvailableActionMap: Record<
|
||||
ActionId,
|
||||
ActionAction & {
|
||||
config?: () => React.ReactNode
|
||||
configInline?: boolean
|
||||
}
|
||||
> = merge(availableActionMap, {
|
||||
rewriteRules: {
|
||||
config: () => (
|
||||
<>
|
||||
<Table className="mt-2">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead size="sm">{t("actions.action_card.from")}</TableHead>
|
||||
<TableHead size="sm">{t("actions.action_card.to")}</TableHead>
|
||||
<TableHead size="sm" className="w-8" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rewriteRules?.map((rule, rewriteIdx) => {
|
||||
const change = (key: "from" | "to", value: string) => {
|
||||
actionActions.updateRewriteRule({
|
||||
index,
|
||||
rewriteRuleIndex: rewriteIdx,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
return (
|
||||
<TableRow key={rewriteIdx}>
|
||||
<TableCell size="sm">
|
||||
<Input
|
||||
disabled={disabled}
|
||||
value={rule.from}
|
||||
className="h-8"
|
||||
onChange={(e) => change("from", e.target.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell size="sm">
|
||||
<Input
|
||||
disabled={disabled}
|
||||
value={rule.to}
|
||||
className="h-8"
|
||||
onChange={(e) => change("to", e.target.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<DeleteTableCell
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.deleteRewriteRule(index, rewriteIdx)
|
||||
}}
|
||||
/>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<AddTableRow
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.addRewriteRule(index)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
webhooks: {
|
||||
config: () => (
|
||||
<>
|
||||
{webhooks?.map((webhook, webhookIdx) => {
|
||||
return (
|
||||
<div key={webhookIdx} className="flex items-center gap-2">
|
||||
<Input
|
||||
disabled={disabled}
|
||||
value={webhook}
|
||||
className="h-8"
|
||||
placeholder="https://"
|
||||
onChange={(e) => {
|
||||
actionActions.updateWebhook({
|
||||
index,
|
||||
webhookIndex: webhookIdx,
|
||||
value: e.target.value,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<DeleteTableCell
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.deleteWebhook(index, webhookIdx)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<AddTableRow
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.addWebhook(index)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
})
|
||||
return Object.values(extendedAvailableActionMap)
|
||||
}, [disabled, index, rewriteRules, t, webhooks])
|
||||
const enabledActions = useMemo(
|
||||
() => availableActions.filter((action) => !!result?.[action.value]),
|
||||
[availableActions, result],
|
||||
)
|
||||
const notEnabledActions = useMemo(
|
||||
() => availableActions.filter((action) => !result?.[action.value]),
|
||||
[availableActions, result],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-full shrink grow space-y-4">
|
||||
<p className="font-medium text-zinc-500">{t("actions.action_card.then_do")}</p>
|
||||
<div className="relative w-full space-y-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
<ActionButton className="hover:text-text absolute right-0 top-0 -translate-y-11 text-zinc-500 duration-200">
|
||||
<i className="i-mgc-add-cute-re" />
|
||||
</ActionButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="left" align="start">
|
||||
{notEnabledActions.map((action) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={action.label}
|
||||
onClick={() => {
|
||||
if (action.onEnable) {
|
||||
action.onEnable(index)
|
||||
} else {
|
||||
actionActions.patchRule(index, { result: { [action.value]: true } })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<i className={action.iconClassname} />
|
||||
{t(action.label)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<section className="pl-6">
|
||||
{enabledActions.map((action, index) => {
|
||||
return (
|
||||
<Fragment key={action.label}>
|
||||
<div className="group/action mt-3 flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className={action.iconClassname} />
|
||||
<span className="shrink grow truncate">{t(action.label)}</span>
|
||||
{action.settingsPath && (
|
||||
<Button
|
||||
buttonClassName="ml-4"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
settingModalPresent(action.settingsPath)
|
||||
}}
|
||||
>
|
||||
{t("actions.action_card.settings")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{action.configInline && action.config && action.config()}
|
||||
|
||||
<Button
|
||||
buttonClassName="absolute opacity-100 group-hover/action:opacity-70 hover:!opacity-100 duration-200 lg:opacity-0 left-0 z-[1] size-5 rounded-full border"
|
||||
variant={"ghost"}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.deleteRuleAction(index, action.value)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-close-cute-re size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
{!action.configInline && action.config && action.config()}
|
||||
{index !== enabledActions.length - 1 && <Divider className="my-2" />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
import { ActionButton, Button } from "@follow/components/ui/button/index.js"
|
||||
import { Divider } from "@follow/components/ui/divider/index.js"
|
||||
import { Input } from "@follow/components/ui/input/index.js"
|
||||
import type { ActionId } from "@follow/models/types"
|
||||
import type { ActionAction } from "@follow/store/action/constant"
|
||||
import { useActionRule } from "@follow/store/action/hooks"
|
||||
import { actionActions } from "@follow/store/action/store"
|
||||
import { merge } from "es-toolkit/compat"
|
||||
import { Fragment, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu/dropdown-menu.js"
|
||||
|
||||
import { useSettingModal } from "../settings/modal/use-setting-modal"
|
||||
import { availableActionMap } from "./constants"
|
||||
|
||||
export const ThenSection = ({ index }: { index: number }) => {
|
||||
const { t } = useTranslation("settings")
|
||||
const result = useActionRule(index, (a) => a.result)
|
||||
|
||||
const rewriteRules = useActionRule(index, (a) => a.result.rewriteRules)
|
||||
const webhooks = useActionRule(index, (a) => a.result.webhooks)
|
||||
const settingModalPresent = useSettingModal()
|
||||
|
||||
const disabled = useActionRule(index, (a) => a.result.disabled)
|
||||
|
||||
const availableActions = useMemo(() => {
|
||||
const extendedAvailableActionMap: Record<
|
||||
ActionId,
|
||||
ActionAction & {
|
||||
config?: () => React.ReactNode
|
||||
}
|
||||
> = merge(availableActionMap, {
|
||||
rewriteRules: {
|
||||
config: () => (
|
||||
<div className="flex flex-col gap-2">
|
||||
{!rewriteRules || rewriteRules.length === 0 ? (
|
||||
<div className="flex items-center justify-between rounded-lg border border-dashed p-3">
|
||||
<span className="text-muted-foreground text-sm">Add rewrite rules</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.addRewriteRule(index)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{rewriteRules.map((rule, rewriteIdx) => {
|
||||
const change = (key: "from" | "to", value: string) => {
|
||||
actionActions.updateRewriteRule({
|
||||
index,
|
||||
rewriteRuleIndex: rewriteIdx,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div key={rewriteIdx} className="group/rewrite-item flex items-end gap-2">
|
||||
<div className="w-full">
|
||||
<label className="text-muted-foreground text-sm">
|
||||
{t("actions.action_card.from")}
|
||||
</label>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
value={rule.from}
|
||||
className="h-8"
|
||||
onChange={(e) => change("from", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<label className="text-muted-foreground text-sm">
|
||||
{t("actions.action_card.to")}
|
||||
</label>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
value={rule.to}
|
||||
className="h-8"
|
||||
onChange={(e) => change("to", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-text-secondary flex gap-1">
|
||||
<ActionButton
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.addRewriteRule(index)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re" />
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.deleteRewriteRule(index, rewriteIdx)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-delete-2-cute-re" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
webhooks: {
|
||||
config: () => (
|
||||
<div className="flex flex-col gap-2">
|
||||
{!webhooks || webhooks.length === 0 ? (
|
||||
<div className="flex items-center justify-between rounded-lg border border-dashed p-3">
|
||||
<span className="text-muted-foreground text-sm">Add webhooks</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.addWebhook(index)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{webhooks.map((webhook, webhookIdx) => {
|
||||
return (
|
||||
<div key={webhookIdx} className="flex items-center gap-2">
|
||||
<Input
|
||||
disabled={disabled}
|
||||
value={webhook}
|
||||
className="h-8"
|
||||
placeholder="https://"
|
||||
onChange={(e) => {
|
||||
actionActions.updateWebhook({
|
||||
index,
|
||||
webhookIndex: webhookIdx,
|
||||
value: e.target.value,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<div className="text-text-secondary flex gap-1">
|
||||
<ActionButton
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.addWebhook(index)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re" />
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.deleteWebhook(index, webhookIdx)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-delete-2-cute-re" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
})
|
||||
return Object.values(extendedAvailableActionMap)
|
||||
}, [disabled, index, rewriteRules, t, webhooks])
|
||||
const enabledActions = useMemo(
|
||||
() => availableActions.filter((action) => !!result?.[action.value]),
|
||||
[availableActions, result],
|
||||
)
|
||||
const notEnabledActions = useMemo(
|
||||
() => availableActions.filter((action) => !result?.[action.value]),
|
||||
[availableActions, result],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-lg font-semibold">{t("actions.action_card.then_do")}</h2>
|
||||
<div className="flex flex-col gap-2 pl-2">
|
||||
{enabledActions.map((action, i) => {
|
||||
return (
|
||||
<Fragment key={action.label}>
|
||||
<div className="group/action relative flex flex-col gap-2 py-2">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className={action.iconClassname} />
|
||||
<span className="shrink grow truncate font-medium">{t(action.label)}</span>
|
||||
{action.prefixElement && <div className="ml-2">{action.prefixElement}</div>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{action.settingsPath && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
settingModalPresent(action.settingsPath)
|
||||
}}
|
||||
>
|
||||
{t("actions.action_card.settings")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
buttonClassName="opacity-0 group-hover/action:opacity-100"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.deleteRuleAction(index, action.value)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-close-cute-re" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{action.config && <div className="pl-6">{action.config()}</div>}
|
||||
</div>
|
||||
{i !== enabledActions.length - 1 && <Divider className="my-1" />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
<Button variant="outline" buttonClassName="mt-2 w-full">
|
||||
<i className="i-mgc-add-cute-re mr-2" />
|
||||
{t("actions.action_card.add")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
{notEnabledActions.map((action) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={action.label}
|
||||
onClick={() => {
|
||||
if (action.onEnable) {
|
||||
action.onEnable(index)
|
||||
} else {
|
||||
actionActions.patchRule(index, { result: { [action.value]: true } })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<i className={action.iconClassname} />
|
||||
{t(action.label)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -13,22 +13,21 @@ import type { ActionFeedField, ActionOperation } from "@follow/models/types"
|
|||
import { filterFieldOptions, filterOperatorOptions } from "@follow/store/action/constant"
|
||||
import { useActionRule } from "@follow/store/action/hooks"
|
||||
import { actionActions } from "@follow/store/action/store"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { Fragment } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ViewSelectContent } from "~/modules/feed/view-select-content"
|
||||
|
||||
export const FeedFilter = ({ index }: { index: number }) => {
|
||||
export const WhenSection = ({ index }: { index: number }) => {
|
||||
const { t } = useTranslation("settings")
|
||||
|
||||
const disabled = useActionRule(index, (a) => a.result.disabled)
|
||||
const condition = useActionRule(index, (a) => a.condition)
|
||||
|
||||
return (
|
||||
<div className="w-full shrink space-y-3 overflow-auto">
|
||||
<p className="font-medium text-zinc-500">{t("actions.action_card.when_feeds_match")}</p>
|
||||
<div className="flex flex-col gap-2 pl-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-lg font-semibold">{t("actions.action_card.when_feeds_match")}</h2>
|
||||
<div className="pl-2">
|
||||
<RadioGroup
|
||||
value={condition.length > 0 ? "filter" : "all"}
|
||||
onValueChange={(value) => {
|
||||
|
|
@ -36,6 +35,7 @@ export const FeedFilter = ({ index }: { index: number }) => {
|
|||
condition: value === "all" ? [] : [[{}]],
|
||||
})
|
||||
}}
|
||||
className="flex gap-4"
|
||||
>
|
||||
<RadioGroupItem disabled={disabled} label={t("actions.action_card.all")} value="all" />
|
||||
<RadioGroupItem
|
||||
|
|
@ -47,38 +47,32 @@ export const FeedFilter = ({ index }: { index: number }) => {
|
|||
</div>
|
||||
|
||||
{condition.length > 0 && (
|
||||
<div className="pl-6">
|
||||
<div className="w-full">
|
||||
<GridHeader />
|
||||
<div className="mt-2">
|
||||
{condition.flatMap((orConditions, orConditionIdx) => {
|
||||
return orConditions.map((condition, conditionIdx) => {
|
||||
const actionConditionIndex = {
|
||||
ruleIndex: index,
|
||||
groupIndex: orConditionIdx,
|
||||
conditionIndex: conditionIdx,
|
||||
}
|
||||
<div className="flex flex-col gap-4 pl-2 pt-4">
|
||||
{condition.map((orConditions, orConditionIdx) => {
|
||||
return (
|
||||
<Fragment key={orConditionIdx}>
|
||||
<div className="@[500px]:p-4 group/or relative flex flex-col gap-2 rounded-lg border p-2 pl-6">
|
||||
<div className="bg-border absolute left-3 top-9 h-[calc(100%-4.5rem)] w-px" />
|
||||
{orConditions.map((condition, conditionIdx) => {
|
||||
const actionConditionIndex = {
|
||||
ruleIndex: index,
|
||||
groupIndex: orConditionIdx,
|
||||
conditionIndex: conditionIdx,
|
||||
}
|
||||
|
||||
const change = (key: string, value: string | number) => {
|
||||
actionActions.pathCondition(actionConditionIndex, {
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
const type =
|
||||
filterFieldOptions.find((option) => option.value === condition.field)?.type ||
|
||||
"text"
|
||||
return (
|
||||
<Fragment key={`${orConditionIdx}${conditionIdx}`}>
|
||||
{conditionIdx === 0 && orConditionIdx !== 0 && (
|
||||
<div className="flex h-16 items-center justify-center">
|
||||
<span className="text-text-secondary text-sm uppercase">
|
||||
{t("actions.action_card.or")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-4 sm:gap-4">
|
||||
<div className="flex items-center justify-between max-sm:flex-row sm:justify-start">
|
||||
<span className="sm:hidden">{t("actions.action_card.field")}</span>
|
||||
const change = (key: string, value: string | number) => {
|
||||
actionActions.pathCondition(actionConditionIndex, {
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
const type =
|
||||
filterFieldOptions.find((option) => option.value === condition.field)?.type ||
|
||||
"text"
|
||||
return (
|
||||
<div className="flex flex-col gap-2" key={conditionIdx}>
|
||||
<div className="group/condition-item relative flex items-center gap-2">
|
||||
<div className="bg-border absolute -left-3.5 top-1/2 h-px w-3 -translate-y-1/2" />
|
||||
<div className="bg-background absolute -left-3.5 top-1/2 size-1.5 -translate-y-1/2 rounded-full border" />
|
||||
<ResponsiveSelect
|
||||
placeholder="Select Field"
|
||||
disabled={disabled}
|
||||
|
|
@ -88,85 +82,75 @@ export const FeedFilter = ({ index }: { index: number }) => {
|
|||
...option,
|
||||
label: t(option.label),
|
||||
}))}
|
||||
triggerClassName="max-sm:w-fit h-8"
|
||||
triggerClassName="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between max-sm:flex-row sm:justify-start">
|
||||
<span className="sm:hidden">{t("actions.action_card.operator")}</span>
|
||||
<OperationSelect
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
value={condition.operator}
|
||||
onValueChange={(value) => change("operator", value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4 max-sm:flex-row sm:justify-start">
|
||||
<span className="sm:hidden">{t("actions.action_card.value")}</span>
|
||||
<ValueInput
|
||||
type={type}
|
||||
value={condition.value}
|
||||
onChange={(value) => change("value", value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
buttonClassName="w-full sm:hidden"
|
||||
variant="outline"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.addConditionItem(actionConditionIndex)
|
||||
}}
|
||||
>
|
||||
{t("actions.action_card.and")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
buttonClassName="w-full max-sm:hidden"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.addConditionItem(actionConditionIndex)
|
||||
}}
|
||||
>
|
||||
{t("actions.action_card.and")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
buttonClassName="opacity-0 group-hover/condition-item:opacity-100"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.deleteConditionItem(actionConditionIndex)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-delete-2-cute-re size-5 text-zinc-600" />
|
||||
<i className="i-mgc-delete-2-cute-re" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{conditionIdx !== orConditions.length - 1 && (
|
||||
<div className="relative my-4 flex w-full items-center justify-center">
|
||||
<div className="relative">
|
||||
<span className="text-text-secondary text-sm uppercase">
|
||||
{conditionIdx !== orConditions.length - 1 && (
|
||||
<div className="flex items-center pl-2">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{t("actions.action_card.and")}
|
||||
</span>
|
||||
<div className="bg-border absolute left-1/2 h-[10px] w-px" />
|
||||
<div className="bg-border absolute left-1/2 top-0 h-[10px] w-px -translate-y-full" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Button
|
||||
variant="outline"
|
||||
buttonClassName="mt-2"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
actionActions.addConditionItem({
|
||||
ruleIndex: index,
|
||||
groupIndex: orConditionIdx,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re mr-2" />
|
||||
{t("actions.action_card.and")}
|
||||
</Button>
|
||||
</div>
|
||||
{orConditionIdx !== condition.length - 1 && (
|
||||
<div className="relative flex h-8 w-full items-center justify-center">
|
||||
<div className="bg-border absolute left-0 top-1/2 h-px w-full" />
|
||||
<span className="text-muted-foreground bg-background z-[1] px-2 text-sm">
|
||||
{t("actions.action_card.or")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
<Button
|
||||
variant="outline"
|
||||
buttonClassName="mt-4 w-full gap-1 py-1"
|
||||
onClick={() => {
|
||||
actionActions.addConditionGroup({ ruleIndex: index })
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<i className="i-mgc-add-cute-re mr-2" />
|
||||
{t("actions.action_card.or")}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -204,7 +188,7 @@ const OperationSelect = ({
|
|||
value={value}
|
||||
onValueChange={(value) => onValueChange?.(value as ActionOperation)}
|
||||
items={options}
|
||||
triggerClassName="h-8 max-sm:w-fit"
|
||||
triggerClassName="h-8"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -220,8 +204,6 @@ const ValueInput = ({
|
|||
onChange: (value: string | number) => void
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
switch (type) {
|
||||
case "view": {
|
||||
return (
|
||||
|
|
@ -230,7 +212,7 @@ const ValueInput = ({
|
|||
onValueChange={(value) => onChange(value)}
|
||||
value={value as string | undefined}
|
||||
>
|
||||
<CommonSelectTrigger className="max-sm:w-fit" />
|
||||
<CommonSelectTrigger />
|
||||
<ViewSelectContent />
|
||||
</Select>
|
||||
)
|
||||
|
|
@ -245,23 +227,30 @@ const ValueInput = ({
|
|||
onValueChange={(value) => onChange(value)}
|
||||
value={value as string | undefined}
|
||||
>
|
||||
<CommonSelectTrigger className="max-sm:w-fit" />
|
||||
<CommonSelectTrigger />
|
||||
<SelectContent>
|
||||
<SelectItem value="collected">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t("words.starred")}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="collected">Collected</SelectItem>
|
||||
<SelectItem value="read">Read</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
case "number": {
|
||||
return (
|
||||
<Input
|
||||
disabled={disabled}
|
||||
type="number"
|
||||
value={value}
|
||||
className="h-8"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<Input
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
value={value}
|
||||
value={value as string | undefined}
|
||||
className="h-8"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
|
|
@ -270,20 +259,8 @@ const ValueInput = ({
|
|||
}
|
||||
}
|
||||
|
||||
const CommonSelectTrigger = ({ className }: { className?: string }) => (
|
||||
<SelectTrigger className={cn("h-8", className)}>
|
||||
const CommonSelectTrigger = () => (
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
)
|
||||
|
||||
const GridHeader = () => {
|
||||
const { t } = useTranslation("settings")
|
||||
return (
|
||||
<div className="text-text-secondary grid grid-cols-4 gap-4 pb-2 text-sm font-medium max-sm:hidden">
|
||||
<div className="pl-1">{t("actions.action_card.field")}</div>
|
||||
<div className="pl-1">{t("actions.action_card.operator")}</div>
|
||||
<div className="pl-1">{t("actions.action_card.value")}</div>
|
||||
<div />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -247,7 +247,7 @@ const SubscriptionFeedsSection = () => {
|
|||
onClick={() => handleSort("date")}
|
||||
type="button"
|
||||
>
|
||||
Date
|
||||
Subscribed Date
|
||||
{sortField === "date" && (
|
||||
<span className="ml-1">{sortDirection === "asc" ? "↑" : "↓"}</span>
|
||||
)}
|
||||
|
|
@ -295,21 +295,6 @@ const SubscriptionFeedsSection = () => {
|
|||
className="sticky bottom-4 flex justify-center"
|
||||
>
|
||||
<div className="bg-material-opaque flex items-center gap-2 rounded px-4 py-2">
|
||||
{/* <DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="text-accent text-xs"
|
||||
type="button"
|
||||
onClick={handleBatchMoveToView}
|
||||
>
|
||||
Add to Category
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent side="top">
|
||||
<CategorySelector />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu> */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<MotionButtonBase className="text-accent text-xs" type="button">
|
||||
|
|
@ -495,12 +480,12 @@ const FeedListItem = memo(
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-text flex items-center gap-1 text-sm opacity-80">
|
||||
{views[subscription.view]!.icon}
|
||||
<span>{tCommon(views[subscription.view]!.name)}</span>
|
||||
</div>
|
||||
{!!subscription.createdAt && (
|
||||
<div className="whitespace-nowrap pr-1 text-center">
|
||||
<div className="whitespace-nowrap pr-1 text-center text-sm">
|
||||
<RelativeDay date={new Date(subscription.createdAt)} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ export type ActionAction = {
|
|||
icon: SFSymbol
|
||||
iconClassname: string
|
||||
settingsPath?: string
|
||||
|
||||
prefixElement?: React.ReactNode
|
||||
}
|
||||
|
||||
export const availableActionMap: Record<ActionId, ActionAction> = {
|
||||
|
|
@ -115,14 +117,12 @@ export const availableActionMap: Record<ActionId, ActionAction> = {
|
|||
label: "actions.action_card.generate_summary",
|
||||
icon: "sparkles",
|
||||
iconClassname: "i-mgc-ai-cute-re",
|
||||
settingsPath: "general",
|
||||
},
|
||||
translation: {
|
||||
value: "translation",
|
||||
label: "actions.action_card.translate_into",
|
||||
icon: "translate",
|
||||
iconClassname: "i-mgc-translate-2-ai-cute-re",
|
||||
settingsPath: "general",
|
||||
},
|
||||
readability: {
|
||||
value: "readability",
|
||||
|
|
|
|||
Loading…
Reference in New Issue