From 9c3acfc1cc6e9b458cf132d7b09dbec79f8d504a Mon Sep 17 00:00:00 2001 From: Kechicode <186776112+Kechicode@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:03:12 +0800 Subject: [PATCH 1/6] feat(DraftDetail): enhance campaign selection UI and logic --- lang/default.json | 12 +- lang/en.json | 12 +- lang/zh-Hans.json | 14 +- lang/zh-Hant.json | 14 +- .../BottomBar/MobileSettingsDialog/index.tsx | 8 +- src/components/Editor/BottomBar/index.tsx | 6 +- .../Editor/SelectCampaign/index.tsx | 173 ++++++++++++------ .../Editor/SettingsDialog/List/index.tsx | 37 +++- .../Editor/SettingsDialog/index.tsx | 6 +- .../Editor/Sidebar/Campaign/index.tsx | 2 +- src/views/Me/DraftDetail/BottomBar.tsx | 15 +- .../Me/DraftDetail/SettingsButton/index.tsx | 15 +- src/views/Me/DraftDetail/Sidebar/index.tsx | 15 +- src/views/Me/DraftDetail/gql.ts | 2 +- 14 files changed, 231 insertions(+), 100 deletions(-) diff --git a/lang/default.json b/lang/default.json index 784c113eba..ec07a834b3 100644 --- a/lang/default.json +++ b/lang/default.json @@ -944,6 +944,9 @@ "defaultMessage": "Verification successful", "description": "src/components/GlobalToast/index.tsx" }, + "DpbBcd": { + "defaultMessage": "Select Activity..." + }, "DqQvtL": { "defaultMessage": "Unblock", "description": "src/views/Me/Settings/Blocked/ToggleBlockButton.tsx" @@ -1565,6 +1568,9 @@ "defaultMessage": "guide", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyOptimismDialog/index.tsx" }, + "P/7t1k": { + "defaultMessage": "Please select a date of activity" + }, "P3y9Bo": { "defaultMessage": "Go to sign", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -1901,9 +1907,6 @@ "defaultMessage": "More", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, - "VrK0Q0": { - "defaultMessage": "Please select..." - }, "VrOoVf": { "defaultMessage": "Matters will never ask your wallet key through any channel.", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -2307,6 +2310,9 @@ "d5+b8r": { "defaultMessage": "Restricted content" }, + "d5bM8A": { + "defaultMessage": "Select Date..." + }, "dAPUJp": { "defaultMessage": "The dazzling light of a meteor shower is enough to illuminate the night sky. The Meteor Canoe badge signifies your participation in the Nomad Matters.", "description": "src/views/User/UserProfile/BadgeNomadLabel/index.tsx" diff --git a/lang/en.json b/lang/en.json index 2fcbe0300c..3db4d8b52d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -944,6 +944,9 @@ "defaultMessage": "Verification successful", "description": "src/components/GlobalToast/index.tsx" }, + "DpbBcd": { + "defaultMessage": "Select Activity..." + }, "DqQvtL": { "defaultMessage": "Unblock", "description": "src/views/Me/Settings/Blocked/ToggleBlockButton.tsx" @@ -1565,6 +1568,9 @@ "defaultMessage": "guide", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyOptimismDialog/index.tsx" }, + "P/7t1k": { + "defaultMessage": "Please select a date of activity" + }, "P3y9Bo": { "defaultMessage": "Go to sign", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -1901,9 +1907,6 @@ "defaultMessage": "More", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, - "VrK0Q0": { - "defaultMessage": "Please select..." - }, "VrOoVf": { "defaultMessage": "Matters will never ask your wallet key through any channel.", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -2307,6 +2310,9 @@ "d5+b8r": { "defaultMessage": "Restricted content" }, + "d5bM8A": { + "defaultMessage": "Select Date..." + }, "dAPUJp": { "defaultMessage": "The dazzling light of a meteor shower is enough to illuminate the night sky. The Meteor Canoe badge signifies your participation in the Nomad Matters.", "description": "src/views/User/UserProfile/BadgeNomadLabel/index.tsx" diff --git a/lang/zh-Hans.json b/lang/zh-Hans.json index 82a99d37af..377a667420 100644 --- a/lang/zh-Hans.json +++ b/lang/zh-Hans.json @@ -529,7 +529,7 @@ "defaultMessage": "上传档案" }, "6pc948": { - "defaultMessage": "投稿七日书自由写" + "defaultMessage": "参与活动" }, "6q0G5e": { "defaultMessage": "加入成功", @@ -944,6 +944,9 @@ "defaultMessage": "验证成功", "description": "src/components/GlobalToast/index.tsx" }, + "DpbBcd": { + "defaultMessage": "选择活动..." + }, "DqQvtL": { "defaultMessage": "解除屏蔽", "description": "src/views/Me/Settings/Blocked/ToggleBlockButton.tsx" @@ -1565,6 +1568,9 @@ "defaultMessage": "教学指南", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyOptimismDialog/index.tsx" }, + "P/7t1k": { + "defaultMessage": "请选定参与活动的投稿日程" + }, "P3y9Bo": { "defaultMessage": "前往签署", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -1901,9 +1907,6 @@ "defaultMessage": "相关推荐", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, - "VrK0Q0": { - "defaultMessage": "请选择⋯" - }, "VrOoVf": { "defaultMessage": "Matters 不会透过任何渠道主动询问你的钱包私钥。", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -2307,6 +2310,9 @@ "d5+b8r": { "defaultMessage": "限制级内容" }, + "d5bM8A": { + "defaultMessage": "投稿日程⋯" + }, "dAPUJp": { "defaultMessage": "流星雨的绚烂光芒足以点亮夜空。流星号徽章纪念你曾参与「游牧者计划」。", "description": "src/views/User/UserProfile/BadgeNomadLabel/index.tsx" diff --git a/lang/zh-Hant.json b/lang/zh-Hant.json index 4d31476398..d08e228708 100644 --- a/lang/zh-Hant.json +++ b/lang/zh-Hant.json @@ -529,7 +529,7 @@ "defaultMessage": "上傳檔案" }, "6pc948": { - "defaultMessage": "投稿七日書自由寫" + "defaultMessage": "參與活動" }, "6q0G5e": { "defaultMessage": "加入成功", @@ -944,6 +944,9 @@ "defaultMessage": "驗證成功", "description": "src/components/GlobalToast/index.tsx" }, + "DpbBcd": { + "defaultMessage": "選擇活動⋯" + }, "DqQvtL": { "defaultMessage": "解除封鎖", "description": "src/views/Me/Settings/Blocked/ToggleBlockButton.tsx" @@ -1565,6 +1568,9 @@ "defaultMessage": "教學指南", "description": "src/components/Forms/PaymentForm/PayTo/SetAmount/SetAmountHeader/WhyOptimismDialog/index.tsx" }, + "P/7t1k": { + "defaultMessage": "請選定參與活動的投稿日程" + }, "P3y9Bo": { "defaultMessage": "前往簽署", "description": "src/components/Forms/PaymentForm/BindWallet/index.tsx" @@ -1901,9 +1907,6 @@ "defaultMessage": "相關推薦", "description": "src/views/ArticleDetail/AuthorSidebar/Tabs/index.tsx" }, - "VrK0Q0": { - "defaultMessage": "請選擇⋯" - }, "VrOoVf": { "defaultMessage": "Matters 不會透過任何渠道主動詢問你的錢包私鑰。", "description": "src/components/Forms/WalletAuthForm/Select.tsx" @@ -2307,6 +2310,9 @@ "d5+b8r": { "defaultMessage": "限制級內容" }, + "d5bM8A": { + "defaultMessage": "投稿日程⋯" + }, "dAPUJp": { "defaultMessage": "流星雨的絢爛光芒足以點亮夜空。流星號徽章紀念你曾參與「遊牧者計畫」。", "description": "src/views/User/UserProfile/BadgeNomadLabel/index.tsx" diff --git a/src/components/Editor/BottomBar/MobileSettingsDialog/index.tsx b/src/components/Editor/BottomBar/MobileSettingsDialog/index.tsx index 7fc405d49e..5b9b9918d1 100644 --- a/src/components/Editor/BottomBar/MobileSettingsDialog/index.tsx +++ b/src/components/Editor/BottomBar/MobileSettingsDialog/index.tsx @@ -20,7 +20,8 @@ const BaseMobileSettingsDialog = ({ children, canComment, toggleComment, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, indented, @@ -62,7 +63,7 @@ const BaseMobileSettingsDialog = ({ {/* campaign */} - {appliedCampaign && editCampaign && ( + {campaigns && selectedCampaign && editCampaign && (

diff --git a/src/components/Editor/BottomBar/index.tsx b/src/components/Editor/BottomBar/index.tsx index 2d1e24f0f5..7f3b7ec1bb 100644 --- a/src/components/Editor/BottomBar/index.tsx +++ b/src/components/Editor/BottomBar/index.tsx @@ -92,7 +92,8 @@ const BottomBar: React.FC = ({ canComment, toggleComment, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, @@ -148,7 +149,8 @@ const BottomBar: React.FC = ({ toggleComment, disableChangeCanComment: article?.canComment, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, diff --git a/src/components/Editor/SelectCampaign/index.tsx b/src/components/Editor/SelectCampaign/index.tsx index 32f8ada3fb..e5cd71d680 100644 --- a/src/components/Editor/SelectCampaign/index.tsx +++ b/src/components/Editor/SelectCampaign/index.tsx @@ -1,9 +1,9 @@ import gql from 'graphql-tag' -import { useContext } from 'react' +import { useContext, useEffect, useState } from 'react' import { FormattedMessage } from 'react-intl' import { datetimeFormat } from '~/common/utils' -import { Form, LanguageContext } from '~/components' +import { Form, LanguageContext, Spacer } from '~/components' import { ArticleCampaignInput, CampaignState, @@ -11,91 +11,151 @@ import { } from '~/gql/graphql' export interface SelectCampaignProps { - appliedCampaign: EditorSelectCampaignFragment - selectedStage?: string + campaigns: EditorSelectCampaignFragment[] + selectedCampaign: EditorSelectCampaignFragment | undefined + selectedStage: string | undefined editCampaign: (value?: ArticleCampaignInput) => any } -export const getSelectCampaign = ({ +export const getSelectCampaigns = ({ applied, attached, createdAt, }: { - applied?: EditorSelectCampaignFragment + applied?: EditorSelectCampaignFragment[] attached: Array<{ campaign: { id: string } stage?: { id: string } | null }> - createdAt: string // draft or article creation time + createdAt: string }) => { - const { start } = applied?.writingPeriod || {} - const isCampaignStarted = !!start && new Date(createdAt) >= new Date(start) - const isCampaignActive = applied?.state === CampaignState.Active + const campaigns = applied?.filter((campaign) => { + const { start, end } = campaign?.writingPeriod || {} + const isCampaignStarted = !!start && new Date(createdAt) >= new Date(start) + const isCampaignEnded = !!end && new Date(createdAt) >= new Date(end) + const isCampaignActive = campaign?.state === CampaignState.Active - // only show appliedCampaign if the article or draft is created during the writing period - const appliedCampaign = - isCampaignStarted && isCampaignActive ? applied : undefined - const selectedCampaign = attached.filter( - (c) => c.campaign.id === applied?.id - )[0] - const selectedStage = selectedCampaign?.stage?.id + // only show appliedCampaign if the article or draft is created during the writing period + return isCampaignStarted && !isCampaignEnded && isCampaignActive + }) + + const selectedCampaign = campaigns?.find((campaign) => { + return attached.find((a) => a.campaign.id === campaign.id) + }) + + const selectedStage = attached.find( + (a) => a.campaign.id === selectedCampaign?.id + )?.stage?.id return { - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, } } const SelectCampaign = ({ - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, }: SelectCampaignProps) => { + const [selectedCampaignId, setSelectedCampaignId] = useState< + string | undefined + >(selectedCampaign?.id) + const [selectedStageId, setSelectedStageId] = useState( + selectedStage + ) + + useEffect(() => { + setSelectedCampaignId(selectedCampaign?.id) + setSelectedStageId(selectedStage) + }, [selectedCampaign, selectedStage]) + const { lang } = useContext(LanguageContext) - const RESET_OPTION = { - name: , + const RESET_CAMPAIGN_OPTION = { + name: , + value: undefined, + selected: !selectedCampaignId, + } + const RESET_STAGE_OPTION = { + name: , value: undefined, - selected: !selectedStage, + selected: !selectedStageId, } const now = new Date() - const availableStages = appliedCampaign.stages.filter((s) => { - const period = s.period + const availableStages = selectedCampaignId + ? campaigns + .find((c) => c.id === selectedCampaignId) + ?.stages.filter((s) => { + const period = s.period - if (!period) return false + if (!period) return false - return now >= new Date(period.start) - }) + return now >= new Date(period.start) + }) + : undefined return ( - - name="select-campaign" - onChange={(option) => - editCampaign( - option.value - ? { campaign: appliedCampaign.id, stage: option.value } - : undefined - ) - } - options={[ - RESET_OPTION, - ...availableStages.reverse().map((s) => { - return { - name: s.period?.start - ? `${s.name} - ${datetimeFormat.absolute({ - date: s.period.start, - lang, - optionalYear: false, - utc8: true, - })}` - : s.name, - value: s.id, - selected: s.id === selectedStage, - } - }), - ]} - size={14} - color="freeWriteBlue" - /> + <> + + name="select-campaign" + onChange={(option) => { + setSelectedCampaignId(option.value) + setSelectedStageId(undefined) + editCampaign( + option.value !== undefined ? { campaign: option.value } : undefined + ) + }} + options={[ + RESET_CAMPAIGN_OPTION, + ...campaigns.map((c) => { + return { + name: c.name, + value: c.id, + selected: c.id === selectedCampaignId, + } + }), + ]} + size={14} + color="freeWriteBlue" + /> + {selectedCampaignId && availableStages && availableStages.length > 0 && ( + <> + + + name="select-stage" + onChange={(option) => { + setSelectedStageId(option.value) + editCampaign( + option.value + ? { campaign: selectedCampaignId, stage: option.value } + : { campaign: selectedCampaignId } + ) + }} + options={[ + RESET_STAGE_OPTION, + ...availableStages.reverse().map((s) => { + return { + name: s.period?.start + ? `${s.name} - ${datetimeFormat.absolute({ + date: s.period.start, + lang, + optionalYear: false, + utc8: true, + })}` + : s.name, + value: s.id, + selected: s.id === selectedStageId, + } + }), + ]} + size={14} + color="freeWriteBlue" + /> + + )} + ) } @@ -103,6 +163,7 @@ SelectCampaign.fragments = gql` fragment EditorSelectCampaign on WritingChallenge { id state + name writingPeriod { start end diff --git a/src/components/Editor/SettingsDialog/List/index.tsx b/src/components/Editor/SettingsDialog/List/index.tsx index 4fad29f003..edfa838f22 100644 --- a/src/components/Editor/SettingsDialog/List/index.tsx +++ b/src/components/Editor/SettingsDialog/List/index.tsx @@ -1,6 +1,6 @@ import { FormattedMessage } from 'react-intl' -import { Dialog } from '~/components' +import { Dialog, toast } from '~/components' import { SetPublishISCNProps } from '~/components/Editor' import ListItem from '../../ListItem' @@ -53,7 +53,8 @@ const SettingsList = ({ collectionCount, tagsCount, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, @@ -68,6 +69,29 @@ const SettingsList = ({ toggleComment, disableChangeCanComment, } + const handleConfirm = () => { + if ( + selectedCampaign && + selectedCampaign.stages.length > 0 && + !selectedStage + ) { + toast.error({ + message: ( + + ), + }) + return + } + + if (onConfirm) { + onConfirm() + } else { + forward('confirm') + } + } return ( <> @@ -78,7 +102,7 @@ const SettingsList = ({ rightBtn={ forward('confirm')} + onClick={handleConfirm} loading={saving} disabled={disabled} /> @@ -108,7 +132,7 @@ const SettingsList = ({ )} - {appliedCampaign && editCampaign && ( + {campaigns && campaigns.length > 0 && editCampaign && (

@@ -187,7 +212,7 @@ const SettingsList = ({ /> forward('confirm')} + onClick={handleConfirm} loading={saving} disabled={disabled} /> diff --git a/src/components/Editor/SettingsDialog/index.tsx b/src/components/Editor/SettingsDialog/index.tsx index 5b3b8ae517..672d44542f 100644 --- a/src/components/Editor/SettingsDialog/index.tsx +++ b/src/components/Editor/SettingsDialog/index.tsx @@ -113,7 +113,8 @@ const BaseEditorSettingsDialog = ({ togglePublishISCN, iscnPublishSaving, - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, @@ -183,7 +184,8 @@ const BaseEditorSettingsDialog = ({ } const campaignProps: Partial = { - appliedCampaign, + campaigns, + selectedCampaign, selectedStage, editCampaign, } diff --git a/src/components/Editor/Sidebar/Campaign/index.tsx b/src/components/Editor/Sidebar/Campaign/index.tsx index 743d94d197..011354311d 100644 --- a/src/components/Editor/Sidebar/Campaign/index.tsx +++ b/src/components/Editor/Sidebar/Campaign/index.tsx @@ -8,7 +8,7 @@ import Box from '../Box' import styles from './styles.module.css' const SidebarCampaign: React.FC> = (props) => { - if (!props.appliedCampaign || !props.editCampaign) { + if (!props.campaigns || props.campaigns.length === 0 || !props.editCampaign) { return null } diff --git a/src/views/Me/DraftDetail/BottomBar.tsx b/src/views/Me/DraftDetail/BottomBar.tsx index ce21c0f591..703e29093e 100644 --- a/src/views/Me/DraftDetail/BottomBar.tsx +++ b/src/views/Me/DraftDetail/BottomBar.tsx @@ -10,7 +10,7 @@ import { import BottomBar from '~/components/Editor/BottomBar' import SupportSettingDialog from '~/components/Editor/MoreSettings/SupportSettingDialog' import { - getSelectCampaign, + getSelectCampaigns, SelectCampaignProps, } from '~/components/Editor/SelectCampaign' import { SidebarIndentProps } from '~/components/Editor/Sidebar/Indent' @@ -71,8 +71,12 @@ const EditDraftBottomBar = ({ const hasOwnCircle = ownCircles && ownCircles.length >= 1 const tags = (draft.tags || []).map(toDigestTagPlaceholder) - const { appliedCampaign, selectedStage } = getSelectCampaign({ - applied: campaigns && campaigns[0], + const { + campaigns: selectableCampaigns, + selectedCampaign, + selectedStage, + } = getSelectCampaigns({ + applied: campaigns, attached: draft.campaigns, createdAt: draft.createdAt, }) @@ -124,9 +128,10 @@ const EditDraftBottomBar = ({ toggleIndent, indentSaving, - appliedCampaign, + campaigns: selectableCampaigns, + selectedCampaign, selectedStage, - editCampaign, + editCampaign: (value) => editCampaign(value as any), } return ( diff --git a/src/views/Me/DraftDetail/SettingsButton/index.tsx b/src/views/Me/DraftDetail/SettingsButton/index.tsx index f8f13e81ce..b9c177325f 100644 --- a/src/views/Me/DraftDetail/SettingsButton/index.tsx +++ b/src/views/Me/DraftDetail/SettingsButton/index.tsx @@ -10,7 +10,7 @@ import { SetTagsProps, } from '~/components/Editor' import { - getSelectCampaign, + getSelectCampaigns, SelectCampaignProps, } from '~/components/Editor/SelectCampaign' import { EditorSettingsDialog } from '~/components/Editor/SettingsDialog' @@ -130,16 +130,21 @@ const SettingsButton = ({ iscnPublishSaving, } - const { appliedCampaign, selectedStage } = getSelectCampaign({ - applied: campaigns && campaigns[0], + const { + campaigns: selectableCampaigns, + selectedCampaign, + selectedStage, + } = getSelectCampaigns({ + applied: campaigns, attached: draft.campaigns, createdAt: draft.createdAt, }) const campaignProps: Partial = { - appliedCampaign, + campaigns: selectableCampaigns, + selectedCampaign, selectedStage, - editCampaign, + editCampaign: (value) => editCampaign(value as any), } const responseProps: SetResponseProps = { diff --git a/src/views/Me/DraftDetail/Sidebar/index.tsx b/src/views/Me/DraftDetail/Sidebar/index.tsx index 816cf36e1f..a2f5b7e951 100644 --- a/src/views/Me/DraftDetail/Sidebar/index.tsx +++ b/src/views/Me/DraftDetail/Sidebar/index.tsx @@ -1,7 +1,7 @@ import { ENTITY_TYPE } from '~/common/enums' import { toDigestTagPlaceholder } from '~/components' import SupportSettingDialog from '~/components/Editor/MoreSettings/SupportSettingDialog' -import { getSelectCampaign } from '~/components/Editor/SelectCampaign' +import { getSelectCampaigns } from '~/components/Editor/SelectCampaign' import Sidebar from '~/components/Editor/Sidebar' import { DigestRichCirclePublicFragment, @@ -144,17 +144,22 @@ const EditDraftIndent = ({ draft }: SidebarProps) => { const EditDraftCampaign = ({ draft, campaigns }: SidebarProps) => { const { edit } = useEditDraftCampaign() - const { appliedCampaign, selectedStage } = getSelectCampaign({ - applied: campaigns && campaigns[0], + const { + campaigns: selectableCampaigns, + selectedCampaign, + selectedStage, + } = getSelectCampaigns({ + applied: campaigns, attached: draft.campaigns, createdAt: draft.createdAt, }) return ( edit(value as any)} /> ) } diff --git a/src/views/Me/DraftDetail/gql.ts b/src/views/Me/DraftDetail/gql.ts index bb11e35494..d720f84404 100644 --- a/src/views/Me/DraftDetail/gql.ts +++ b/src/views/Me/DraftDetail/gql.ts @@ -61,7 +61,7 @@ export const DRAFT_DETAIL_VIEWER = gql` query DraftDetailViewerQuery { viewer { id - campaigns(input: { first: 1 }) { + campaigns(input: { first: null }) { edges { node { id From 8deabbcda498d51e9052d60885b89193c99012aa Mon Sep 17 00:00:00 2001 From: Kechicode <186776112+Kechicode@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:26:57 +0800 Subject: [PATCH 2/6] feat(Edit): enhance campaign selection UI and logic --- src/views/ArticleDetail/Edit/Header/index.tsx | 5 ++- src/views/ArticleDetail/Edit/gql.ts | 2 +- src/views/ArticleDetail/Edit/index.tsx | 38 ++++++++++++++----- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/views/ArticleDetail/Edit/Header/index.tsx b/src/views/ArticleDetail/Edit/Header/index.tsx index 66e4ddddcc..f990cfea5d 100644 --- a/src/views/ArticleDetail/Edit/Header/index.tsx +++ b/src/views/ArticleDetail/Edit/Header/index.tsx @@ -97,10 +97,11 @@ const EditModeHeader = ({ const isSensitiveRevised = restProps.contentSensitive !== article.sensitiveByAuthor const isCampaignRevised = + restProps.selectedCampaign?.id !== article.campaigns[0].campaign.id || restProps.selectedStage !== article.campaigns[0]?.stage?.id const isResetCampaign = isCampaignRevised && - (!restProps.appliedCampaign?.id || !restProps.selectedStage) + (!restProps.selectedCampaign?.id || !restProps.selectedStage) const needRepublish = isTitleRevised || @@ -165,7 +166,7 @@ const EditModeHeader = ({ ? [] : [ { - campaign: restProps.appliedCampaign?.id, + campaign: restProps.selectedCampaign?.id, stage: restProps.selectedStage, }, ], diff --git a/src/views/ArticleDetail/Edit/gql.ts b/src/views/ArticleDetail/Edit/gql.ts index 914eb03b27..3926c0ea22 100644 --- a/src/views/ArticleDetail/Edit/gql.ts +++ b/src/views/ArticleDetail/Edit/gql.ts @@ -35,7 +35,7 @@ export const GET_EDIT_ARTICLE = gql` ownCircles { ...DigestRichCirclePublic } - campaigns(input: { first: 1 }) { + campaigns(input: { first: null }) { edges { node { id diff --git a/src/views/ArticleDetail/Edit/index.tsx b/src/views/ArticleDetail/Edit/index.tsx index 1b88e284ab..2cb1341de7 100644 --- a/src/views/ArticleDetail/Edit/index.tsx +++ b/src/views/ArticleDetail/Edit/index.tsx @@ -31,7 +31,7 @@ import { import BottomBar from '~/components/Editor/BottomBar' import SupportSettingDialog from '~/components/Editor/MoreSettings/SupportSettingDialog' import { - getSelectCampaign, + getSelectCampaigns, SelectCampaignProps, } from '~/components/Editor/SelectCampaign' import Sidebar from '~/components/Editor/Sidebar' @@ -53,6 +53,7 @@ import { DigestTagFragment, DirectImageUploadDoneMutation, DirectImageUploadMutation, + EditorSelectCampaignFragment, QueryEditArticleAssetsQuery, QueryEditArticleQuery, SingleFileUploadMutation, @@ -63,7 +64,7 @@ import EditHeader from './Header' import PublishState from './PublishState' import styles from './styles.module.css' -type Article = NonNullable< +export type Article = NonNullable< QueryEditArticleQuery['article'] & { __typename: 'Article' } @@ -142,21 +143,39 @@ const BaseEdit = ({ article }: { article: Article }) => { // campaign const appliedCampaigns = article.author.campaigns.edges?.map((e) => e.node) - const { appliedCampaign, selectedStage } = getSelectCampaign({ - applied: appliedCampaigns && appliedCampaigns[0], + const { + campaigns: selectableCampaigns, + selectedCampaign: _selectedCampaign, + selectedStage: _selectedStage, + } = getSelectCampaigns({ + applied: appliedCampaigns, attached: article.campaigns, createdAt: article.createdAt, }) const [campaign, setCampaign] = useState( - appliedCampaign?.id && selectedStage + _selectedCampaign?.id && _selectedStage ? { - campaign: appliedCampaign.id, - stage: selectedStage, + campaign: _selectedCampaign.id, + stage: _selectedStage, } : undefined ) + const [selectedCampaign, setSelectedCampaign] = useState< + EditorSelectCampaignFragment | undefined + >(_selectedCampaign) + const [selectedStage, setSelectedStage] = useState( + _selectedStage + ) + + useEffect(() => { + setSelectedCampaign( + selectableCampaigns?.find((c) => c.id === campaign?.campaign) + ) + setSelectedStage(campaign?.stage || undefined) + }, [campaign]) + const [requestForDonation, setRequestForDonation] = useState( article.requestForDonation ) @@ -212,8 +231,9 @@ const BaseEdit = ({ article }: { article: Article }) => { indentSaving: false, } const campaignProps: Partial = { - appliedCampaign, - selectedStage: campaign?.stage, + campaigns: selectableCampaigns, + selectedCampaign: selectedCampaign, + selectedStage: selectedStage, editCampaign: setCampaign, } From 5f67c9691a0c2ce56e87c45f6ae7d472f1d918f7 Mon Sep 17 00:00:00 2001 From: Kechicode <186776112+Kechicode@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:27:10 +0800 Subject: [PATCH 3/6] feat(SelectCampaign): remove redundant code --- .../Editor/SelectCampaign/index.tsx | 97 ++++++++----------- 1 file changed, 42 insertions(+), 55 deletions(-) diff --git a/src/components/Editor/SelectCampaign/index.tsx b/src/components/Editor/SelectCampaign/index.tsx index e5cd71d680..0761f901ca 100644 --- a/src/components/Editor/SelectCampaign/index.tsx +++ b/src/components/Editor/SelectCampaign/index.tsx @@ -1,5 +1,5 @@ import gql from 'graphql-tag' -import { useContext, useEffect, useState } from 'react' +import { useContext } from 'react' import { FormattedMessage } from 'react-intl' import { datetimeFormat } from '~/common/utils' @@ -60,33 +60,21 @@ const SelectCampaign = ({ selectedStage, editCampaign, }: SelectCampaignProps) => { - const [selectedCampaignId, setSelectedCampaignId] = useState< - string | undefined - >(selectedCampaign?.id) - const [selectedStageId, setSelectedStageId] = useState( - selectedStage - ) - - useEffect(() => { - setSelectedCampaignId(selectedCampaign?.id) - setSelectedStageId(selectedStage) - }, [selectedCampaign, selectedStage]) - const { lang } = useContext(LanguageContext) const RESET_CAMPAIGN_OPTION = { name: , value: undefined, - selected: !selectedCampaignId, + selected: !selectedCampaign?.id, } const RESET_STAGE_OPTION = { name: , value: undefined, - selected: !selectedStageId, + selected: !selectedStage, } const now = new Date() - const availableStages = selectedCampaignId + const availableStages = selectedCampaign?.id ? campaigns - .find((c) => c.id === selectedCampaignId) + .find((c) => c.id === selectedCampaign.id) ?.stages.filter((s) => { const period = s.period @@ -101,8 +89,6 @@ const SelectCampaign = ({ name="select-campaign" onChange={(option) => { - setSelectedCampaignId(option.value) - setSelectedStageId(undefined) editCampaign( option.value !== undefined ? { campaign: option.value } : undefined ) @@ -113,48 +99,49 @@ const SelectCampaign = ({ return { name: c.name, value: c.id, - selected: c.id === selectedCampaignId, + selected: c.id === selectedCampaign?.id, } }), ]} size={14} color="freeWriteBlue" /> - {selectedCampaignId && availableStages && availableStages.length > 0 && ( - <> - - - name="select-stage" - onChange={(option) => { - setSelectedStageId(option.value) - editCampaign( - option.value - ? { campaign: selectedCampaignId, stage: option.value } - : { campaign: selectedCampaignId } - ) - }} - options={[ - RESET_STAGE_OPTION, - ...availableStages.reverse().map((s) => { - return { - name: s.period?.start - ? `${s.name} - ${datetimeFormat.absolute({ - date: s.period.start, - lang, - optionalYear: false, - utc8: true, - })}` - : s.name, - value: s.id, - selected: s.id === selectedStageId, - } - }), - ]} - size={14} - color="freeWriteBlue" - /> - - )} + {selectedCampaign?.id && + availableStages && + availableStages.length > 0 && ( + <> + + + name="select-stage" + onChange={(option) => { + editCampaign( + option.value + ? { campaign: selectedCampaign.id, stage: option.value } + : { campaign: selectedCampaign.id } + ) + }} + options={[ + RESET_STAGE_OPTION, + ...availableStages.reverse().map((s) => { + return { + name: s.period?.start + ? `${s.name} - ${datetimeFormat.absolute({ + date: s.period.start, + lang, + optionalYear: false, + utc8: true, + })}` + : s.name, + value: s.id, + selected: s.id === selectedStage, + } + }), + ]} + size={14} + color="freeWriteBlue" + /> + + )} ) } From 7fcbaba5648a44f0999b6906cf421a4857fd4102 Mon Sep 17 00:00:00 2001 From: Kechicode <186776112+Kechicode@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:51:01 +0800 Subject: [PATCH 4/6] fix(ArticleDetail): handle optional campaign check in article edit header --- src/views/ArticleDetail/Edit/Header/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/ArticleDetail/Edit/Header/index.tsx b/src/views/ArticleDetail/Edit/Header/index.tsx index f990cfea5d..105271b6e0 100644 --- a/src/views/ArticleDetail/Edit/Header/index.tsx +++ b/src/views/ArticleDetail/Edit/Header/index.tsx @@ -97,7 +97,7 @@ const EditModeHeader = ({ const isSensitiveRevised = restProps.contentSensitive !== article.sensitiveByAuthor const isCampaignRevised = - restProps.selectedCampaign?.id !== article.campaigns[0].campaign.id || + restProps.selectedCampaign?.id !== article.campaigns[0]?.campaign.id || restProps.selectedStage !== article.campaigns[0]?.stage?.id const isResetCampaign = isCampaignRevised && From 8c88bace3cfe3000bbb3950d28d5d716cac4ff9f Mon Sep 17 00:00:00 2001 From: Kechicode <186776112+Kechicode@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:57:54 +0800 Subject: [PATCH 5/6] refactor(ArticleDetail): extract campaign state logic into custom hook --- .../Edit/Hooks/useCampaignState.ts | 56 +++++++++++++++++++ src/views/ArticleDetail/Edit/index.tsx | 48 ++-------------- 2 files changed, 62 insertions(+), 42 deletions(-) create mode 100644 src/views/ArticleDetail/Edit/Hooks/useCampaignState.ts diff --git a/src/views/ArticleDetail/Edit/Hooks/useCampaignState.ts b/src/views/ArticleDetail/Edit/Hooks/useCampaignState.ts new file mode 100644 index 0000000000..5fa16c298d --- /dev/null +++ b/src/views/ArticleDetail/Edit/Hooks/useCampaignState.ts @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react' + +import { getSelectCampaigns } from '~/components/Editor/SelectCampaign' +import { + ArticleCampaignInput, + EditorSelectCampaignFragment, +} from '~/gql/graphql' + +import { Article } from '../index' + +export const useCampaignState = (article: Article) => { + const appliedCampaigns = article.author.campaigns.edges?.map((e) => e.node) + + const { + campaigns: selectableCampaigns, + selectedCampaign: initialSelectedCampaign, + selectedStage: initialSelectedStage, + } = getSelectCampaigns({ + applied: appliedCampaigns, + attached: article.campaigns, + createdAt: article.createdAt, + }) + + const [campaign, setCampaign] = useState( + initialSelectedCampaign?.id && initialSelectedStage + ? { + campaign: initialSelectedCampaign.id, + stage: initialSelectedStage, + } + : undefined + ) + + // UI state for selected campaign/stage + const [selectedCampaign, setSelectedCampaign] = useState< + EditorSelectCampaignFragment | undefined + >(initialSelectedCampaign) + const [selectedStage, setSelectedStage] = useState( + initialSelectedStage + ) + + // Keep UI state in sync with campaign input + useEffect(() => { + setSelectedCampaign( + selectableCampaigns?.find((c) => c.id === campaign?.campaign) + ) + setSelectedStage(campaign?.stage || undefined) + }, [campaign, selectableCampaigns]) + + return { + campaign, + setCampaign, + selectedCampaign, + selectedStage, + selectableCampaigns, + } +} diff --git a/src/views/ArticleDetail/Edit/index.tsx b/src/views/ArticleDetail/Edit/index.tsx index 2cb1341de7..3f1f8d4005 100644 --- a/src/views/ArticleDetail/Edit/index.tsx +++ b/src/views/ArticleDetail/Edit/index.tsx @@ -30,10 +30,7 @@ import { } from '~/components/Editor' import BottomBar from '~/components/Editor/BottomBar' import SupportSettingDialog from '~/components/Editor/MoreSettings/SupportSettingDialog' -import { - getSelectCampaigns, - SelectCampaignProps, -} from '~/components/Editor/SelectCampaign' +import { SelectCampaignProps } from '~/components/Editor/SelectCampaign' import Sidebar from '~/components/Editor/Sidebar' import { SidebarIndentProps } from '~/components/Editor/Sidebar/Indent' import { QueryError, useImperativeQuery } from '~/components/GQL' @@ -44,7 +41,6 @@ import { } from '~/components/GQL/mutations/uploadFile' import { ArticleAccessType, - ArticleCampaignInput, ArticleDigestDropdownArticleFragment, ArticleLicenseType, AssetFragment, @@ -53,7 +49,6 @@ import { DigestTagFragment, DirectImageUploadDoneMutation, DirectImageUploadMutation, - EditorSelectCampaignFragment, QueryEditArticleAssetsQuery, QueryEditArticleQuery, SingleFileUploadMutation, @@ -61,6 +56,7 @@ import { import { GET_EDIT_ARTICLE, GET_EDIT_ARTICLE_ASSETS } from './gql' import EditHeader from './Header' +import { useCampaignState } from './Hooks/useCampaignState' import PublishState from './PublishState' import styles from './styles.module.css' @@ -141,40 +137,8 @@ const BaseEdit = ({ article }: { article: Article }) => { setLicense(newLicense) } - // campaign - const appliedCampaigns = article.author.campaigns.edges?.map((e) => e.node) - const { - campaigns: selectableCampaigns, - selectedCampaign: _selectedCampaign, - selectedStage: _selectedStage, - } = getSelectCampaigns({ - applied: appliedCampaigns, - attached: article.campaigns, - createdAt: article.createdAt, - }) - - const [campaign, setCampaign] = useState( - _selectedCampaign?.id && _selectedStage - ? { - campaign: _selectedCampaign.id, - stage: _selectedStage, - } - : undefined - ) - - const [selectedCampaign, setSelectedCampaign] = useState< - EditorSelectCampaignFragment | undefined - >(_selectedCampaign) - const [selectedStage, setSelectedStage] = useState( - _selectedStage - ) - - useEffect(() => { - setSelectedCampaign( - selectableCampaigns?.find((c) => c.id === campaign?.campaign) - ) - setSelectedStage(campaign?.stage || undefined) - }, [campaign]) + const { setCampaign, selectedCampaign, selectedStage, selectableCampaigns } = + useCampaignState(article) const [requestForDonation, setRequestForDonation] = useState( article.requestForDonation @@ -232,8 +196,8 @@ const BaseEdit = ({ article }: { article: Article }) => { } const campaignProps: Partial = { campaigns: selectableCampaigns, - selectedCampaign: selectedCampaign, - selectedStage: selectedStage, + selectedCampaign, + selectedStage, editCampaign: setCampaign, } From d9d67128d1ffdd1e3d2d37c132820809d1ecd8d0 Mon Sep 17 00:00:00 2001 From: Kechicode <186776112+Kechicode@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:26:22 +0800 Subject: [PATCH 6/6] fix(lang): fix copy --- lang/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en.json b/lang/en.json index 3db4d8b52d..828e86a159 100644 --- a/lang/en.json +++ b/lang/en.json @@ -529,7 +529,7 @@ "defaultMessage": "Upload file" }, "6pc948": { - "defaultMessage": "Add to FreeWrite" + "defaultMessage": "Add to Free Write" }, "6q0G5e": { "defaultMessage": "Successfully added",