diff --git a/apps/desktop/layer/renderer/src/modules/action/action-card.tsx b/apps/desktop/layer/renderer/src/modules/action/action-card.tsx deleted file mode 100644 index 61a55b665..000000000 --- a/apps/desktop/layer/renderer/src/modules/action/action-card.tsx +++ /dev/null @@ -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 ( - - - - { - actionActions.deleteRule(index) - }} - > - - {t("words.delete")} - - {t("words.delete")} - - - - - - - - - ) -} - -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 ( -
-

{t("actions.action_card.name")}

- { - actionActions.patchRule(index, { name: e.target.value }) - }} - /> -
- - { - actionActions.patchRule(index, { - result: { disabled: !checked }, - }) - }} - /> -
- ) -} diff --git a/apps/desktop/layer/renderer/src/modules/action/action-setting.tsx b/apps/desktop/layer/renderer/src/modules/action/action-setting.tsx index bd876d9d5..f208fa182 100644 --- a/apps/desktop/layer/renderer/src/modules/action/action-setting.tsx +++ b/apps/desktop/layer/renderer/src/modules/action/action-setting.tsx @@ -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 } size="large" /> } return ( -
- {Array.from({ length: actionLength }).map((_action, actionIdx) => ( - - ))} - +
+ +
+ {actions.map((_, actionIdx) => { + return + })} +
) } -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 ( -
+
-
diff --git a/apps/desktop/layer/renderer/src/modules/action/constants.tsx b/apps/desktop/layer/renderer/src/modules/action/constants.tsx new file mode 100644 index 000000000..9c60ebde1 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/action/constants.tsx @@ -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: , + }, + translation: { + ...availableActionMapOriginal.translation, + prefixElement: , + }, +} + +function AiTargetLanguageSelector() { + const { t } = useTranslation("settings") + const actionLanguage = useGeneralSettingKey("actionLanguage") + + return ( + { + 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, + })), + ]} + /> + ) +} diff --git a/apps/desktop/layer/renderer/src/modules/action/rule-card.tsx b/apps/desktop/layer/renderer/src/modules/action/rule-card.tsx new file mode 100644 index 000000000..9fdb3aa42 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/action/rule-card.tsx @@ -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 ( +
+ +
+ + +
+
+ ) +} + +const RuleCardToolbar = ({ index }: { index: number }) => { + const name = useActionRule(index, (a) => a.name) + const disabled = useActionRule(index, (a) => a.result.disabled) + + return ( +
+ { + actionActions.patchRule(index, { name: e.target.value }) + }} + /> +
+ + { + actionActions.patchRule(index, { + result: { disabled: !checked }, + }) + }} + /> +
+ +
+
+ ) +} diff --git a/apps/desktop/layer/renderer/src/modules/action/target-action-list.tsx b/apps/desktop/layer/renderer/src/modules/action/target-action-list.tsx deleted file mode 100644 index 77a565a28..000000000 --- a/apps/desktop/layer/renderer/src/modules/action/target-action-list.tsx +++ /dev/null @@ -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 ( - - ) -} - -const DeleteTableCell = ({ disabled, onClick }: { disabled?: boolean; onClick?: () => void }) => ( - - - - - -) - -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: () => ( - <> - - - - {t("actions.action_card.from")} - {t("actions.action_card.to")} - - - - - {rewriteRules?.map((rule, rewriteIdx) => { - const change = (key: "from" | "to", value: string) => { - actionActions.updateRewriteRule({ - index, - rewriteRuleIndex: rewriteIdx, - key, - value, - }) - } - return ( - - - change("from", e.target.value)} - /> - - - change("to", e.target.value)} - /> - - { - actionActions.deleteRewriteRule(index, rewriteIdx) - }} - /> - - ) - })} - -
- { - actionActions.addRewriteRule(index) - }} - /> - - ), - }, - webhooks: { - config: () => ( - <> - {webhooks?.map((webhook, webhookIdx) => { - return ( -
- { - actionActions.updateWebhook({ - index, - webhookIndex: webhookIdx, - value: e.target.value, - }) - }} - /> - { - actionActions.deleteWebhook(index, webhookIdx) - }} - /> -
- ) - })} - { - 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 ( -
-

{t("actions.action_card.then_do")}

-
- - - - - - - - {notEnabledActions.map((action) => { - return ( - { - if (action.onEnable) { - action.onEnable(index) - } else { - actionActions.patchRule(index, { result: { [action.value]: true } }) - } - }} - > -
- - {t(action.label)} -
-
- ) - })} -
-
- -
- {enabledActions.map((action, index) => { - return ( - -
-
- - {t(action.label)} - {action.settingsPath && ( - - )} -
- {action.configInline && action.config && action.config()} - - -
- {!action.configInline && action.config && action.config()} - {index !== enabledActions.length - 1 && } -
- ) - })} -
-
-
- ) -} diff --git a/apps/desktop/layer/renderer/src/modules/action/then-section.tsx b/apps/desktop/layer/renderer/src/modules/action/then-section.tsx new file mode 100644 index 000000000..f6c9365ec --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/action/then-section.tsx @@ -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: () => ( +
+ {!rewriteRules || rewriteRules.length === 0 ? ( +
+ Add rewrite rules + +
+ ) : ( + <> + {rewriteRules.map((rule, rewriteIdx) => { + const change = (key: "from" | "to", value: string) => { + actionActions.updateRewriteRule({ + index, + rewriteRuleIndex: rewriteIdx, + key, + value, + }) + } + return ( +
+
+ + change("from", e.target.value)} + /> +
+
+ + change("to", e.target.value)} + /> +
+
+ { + actionActions.addRewriteRule(index) + }} + > + + + { + actionActions.deleteRewriteRule(index, rewriteIdx) + }} + > + + +
+
+ ) + })} + + )} +
+ ), + }, + webhooks: { + config: () => ( +
+ {!webhooks || webhooks.length === 0 ? ( +
+ Add webhooks + +
+ ) : ( + <> + {webhooks.map((webhook, webhookIdx) => { + return ( +
+ { + actionActions.updateWebhook({ + index, + webhookIndex: webhookIdx, + value: e.target.value, + }) + }} + /> +
+ { + actionActions.addWebhook(index) + }} + > + + + { + actionActions.deleteWebhook(index, webhookIdx) + }} + > + + +
+
+ ) + })} + + )} +
+ ), + }, + }) + 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 ( +
+

{t("actions.action_card.then_do")}

+
+ {enabledActions.map((action, i) => { + return ( + +
+
+
+ + {t(action.label)} + {action.prefixElement &&
{action.prefixElement}
} +
+
+ {action.settingsPath && ( + + )} + +
+
+ {action.config &&
{action.config()}
} +
+ {i !== enabledActions.length - 1 && } +
+ ) + })} + + + + + + {notEnabledActions.map((action) => { + return ( + { + if (action.onEnable) { + action.onEnable(index) + } else { + actionActions.patchRule(index, { result: { [action.value]: true } }) + } + }} + > +
+ + {t(action.label)} +
+
+ ) + })} +
+
+
+
+ ) +} diff --git a/apps/desktop/layer/renderer/src/modules/action/feed-filter.tsx b/apps/desktop/layer/renderer/src/modules/action/when-section.tsx similarity index 51% rename from apps/desktop/layer/renderer/src/modules/action/feed-filter.tsx rename to apps/desktop/layer/renderer/src/modules/action/when-section.tsx index d71fced8d..4461c3aa8 100644 --- a/apps/desktop/layer/renderer/src/modules/action/feed-filter.tsx +++ b/apps/desktop/layer/renderer/src/modules/action/when-section.tsx @@ -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 ( -
-

{t("actions.action_card.when_feeds_match")}

-
+
+

{t("actions.action_card.when_feeds_match")}

+
0 ? "filter" : "all"} onValueChange={(value) => { @@ -36,6 +35,7 @@ export const FeedFilter = ({ index }: { index: number }) => { condition: value === "all" ? [] : [[{}]], }) }} + className="flex gap-4" > {
{condition.length > 0 && ( -
-
- -
- {condition.flatMap((orConditions, orConditionIdx) => { - return orConditions.map((condition, conditionIdx) => { - const actionConditionIndex = { - ruleIndex: index, - groupIndex: orConditionIdx, - conditionIndex: conditionIdx, - } +
+ {condition.map((orConditions, orConditionIdx) => { + return ( + +
+
+ {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 ( - - {conditionIdx === 0 && orConditionIdx !== 0 && ( -
- - {t("actions.action_card.or")} - -
- )} -
-
- {t("actions.action_card.field")} + const change = (key: string, value: string | number) => { + actionActions.pathCondition(actionConditionIndex, { + [key]: value, + }) + } + const type = + filterFieldOptions.find((option) => option.value === condition.field)?.type || + "text" + return ( +
+
+
+
{ ...option, label: t(option.label), }))} - triggerClassName="max-sm:w-fit h-8" + triggerClassName="h-8" /> -
-
- {t("actions.action_card.operator")} change("operator", value)} /> -
-
- {t("actions.action_card.value")} change("value", value)} disabled={disabled} /> -
- -
- -
-
- {conditionIdx !== orConditions.length - 1 && ( -
-
- + {conditionIdx !== orConditions.length - 1 && ( +
+ {t("actions.action_card.and")} -
-
-
- )} - - ) - }) - })} -
-
+ )} +
+ ) + })} + +
+ {orConditionIdx !== condition.length - 1 && ( +
+
+ + {t("actions.action_card.or")} + +
+ )} + + ) + })}
@@ -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} > - + ) @@ -245,23 +227,30 @@ const ValueInput = ({ onValueChange={(value) => onChange(value)} value={value as string | undefined} > - + - -
- {t("words.starred")} -
-
+ Collected + Read
) } + case "number": { + return ( + onChange(e.target.value)} + /> + ) + } default: { return ( onChange(e.target.value)} /> @@ -270,20 +259,8 @@ const ValueInput = ({ } } -const CommonSelectTrigger = ({ className }: { className?: string }) => ( - +const CommonSelectTrigger = () => ( + ) - -const GridHeader = () => { - const { t } = useTranslation("settings") - return ( -
-
{t("actions.action_card.field")}
-
{t("actions.action_card.operator")}
-
{t("actions.action_card.value")}
-
-
- ) -} diff --git a/apps/desktop/layer/renderer/src/modules/settings/tabs/feeds.tsx b/apps/desktop/layer/renderer/src/modules/settings/tabs/feeds.tsx index d3a09d430..f17278ca0 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/tabs/feeds.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/tabs/feeds.tsx @@ -247,7 +247,7 @@ const SubscriptionFeedsSection = () => { onClick={() => handleSort("date")} type="button" > - Date + Subscribed Date {sortField === "date" && ( {sortDirection === "asc" ? "↑" : "↓"} )} @@ -295,21 +295,6 @@ const SubscriptionFeedsSection = () => { className="sticky bottom-4 flex justify-center" >
- {/* - - - - - - - - */} @@ -495,12 +480,12 @@ const FeedListItem = memo(
-
+
{views[subscription.view]!.icon} {tCommon(views[subscription.view]!.name)}
{!!subscription.createdAt && ( -
+
)} diff --git a/packages/internal/store/src/action/constant.ts b/packages/internal/store/src/action/constant.ts index e5ba99f07..b66c1d624 100644 --- a/packages/internal/store/src/action/constant.ts +++ b/packages/internal/store/src/action/constant.ts @@ -107,6 +107,8 @@ export type ActionAction = { icon: SFSymbol iconClassname: string settingsPath?: string + + prefixElement?: React.ReactNode } export const availableActionMap: Record = { @@ -115,14 +117,12 @@ export const availableActionMap: Record = { 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",