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:
Innei 2025-06-16 16:52:17 +08:00
parent 95b23c3be7
commit 1ace5eaaa3
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
9 changed files with 486 additions and 481 deletions

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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,
})),
]}
/>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)}

View File

@ -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",