From 6245551fe83898e21a6f4ee82561f69c33ff4098 Mon Sep 17 00:00:00 2001 From: keyansheng <65121402+keyansheng@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:32:58 +0800 Subject: [PATCH 1/6] Add story statuses --- src/commons/application/ApplicationTypes.ts | 7 +++- src/commons/sagas/StoriesSaga.ts | 37 +++++++++++++++++-- src/features/stories/StoriesActions.ts | 4 +- src/features/stories/StoriesReducer.ts | 2 +- src/features/stories/StoriesTypes.ts | 18 ++++++++- .../storiesComponents/BackendAccess.ts | 26 ++++++++++--- src/pages/stories/Stories.tsx | 28 +++++++------- 7 files changed, 94 insertions(+), 28 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 2015c1007c..4ee1b0e03f 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -534,7 +534,12 @@ export const defaultSession: SessionState = { }; export const defaultStories: StoriesState = { - storyList: [], + storyLists: { + draft: [], + pending: [], + rejected: [], + published: [] + }, currentStoryId: null, currentStory: null, envs: {} diff --git a/src/commons/sagas/StoriesSaga.ts b/src/commons/sagas/StoriesSaga.ts index 67696b7e53..39d8c44e0f 100644 --- a/src/commons/sagas/StoriesSaga.ts +++ b/src/commons/sagas/StoriesSaga.ts @@ -19,6 +19,8 @@ import { SET_CURRENT_STORY_ID, StoryData, StoryListView, + StoryListViews, + StoryStatus, StoryView } from 'src/features/stories/StoriesTypes'; @@ -33,11 +35,34 @@ import { safeTakeEvery as takeEvery } from './SafeEffects'; export function* storiesSaga(): SagaIterator { yield takeLatest(GET_STORIES_LIST, function* () { const tokens: Tokens = yield selectTokens(); - const allStories: StoryListView[] = yield call(async () => { - const resp = await getStories(tokens); + + const draftStories: StoryListView[] = yield call(async () => { + const resp = await getStories(tokens, StoryStatus.Draft); + return resp ?? []; + }); + + const pendingStories: StoryListView[] = yield call(async () => { + const resp = await getStories(tokens, StoryStatus.Pending); + return resp ?? []; + }); + + const rejectedStories: StoryListView[] = yield call(async () => { + const resp = await getStories(tokens, StoryStatus.Rejected); + return resp ?? []; + }); + + const publishedStories: StoryListView[] = yield call(async () => { + const resp = await getStories(tokens, StoryStatus.Published); return resp ?? []; }); + const allStories: StoryListViews = { + draft: draftStories, + pending: pendingStories, + rejected: rejectedStories, + published: publishedStories + }; + yield put(actions.updateStoriesList(allStories)); }); @@ -55,7 +80,9 @@ export function* storiesSaga(): SagaIterator { const defaultStory: StoryData = { title: '', content: defaultStoryContent, - pinOrder: null + pinOrder: null, + status: StoryStatus.Draft, + statusMessage: '' }; yield put(actions.setCurrentStory(defaultStory)); } @@ -98,7 +125,9 @@ export function* storiesSaga(): SagaIterator { id, story.title, story.content, - story.pinOrder + story.pinOrder, + story.status, + story.statusMessage ); // TODO: Check correctness diff --git a/src/features/stories/StoriesActions.ts b/src/features/stories/StoriesActions.ts index c2c7a61907..f766513a4e 100644 --- a/src/features/stories/StoriesActions.ts +++ b/src/features/stories/StoriesActions.ts @@ -21,7 +21,7 @@ import { SET_CURRENT_STORY, SET_CURRENT_STORY_ID, StoryData, - StoryListView, + StoryListViews, StoryParams, TOGGLE_STORIES_USING_SUBST, UPDATE_STORIES_LIST @@ -70,7 +70,7 @@ export const toggleStoriesUsingSubst = createAction( export const getStoriesList = createAction(GET_STORIES_LIST, () => ({ payload: {} })); export const updateStoriesList = createAction( UPDATE_STORIES_LIST, - (storyList: StoryListView[]) => ({ payload: storyList }) + (storyLists: StoryListViews) => ({ payload: storyLists }) ); export const setCurrentStory = createAction(SET_CURRENT_STORY, (story: StoryData | null) => ({ payload: story diff --git a/src/features/stories/StoriesReducer.ts b/src/features/stories/StoriesReducer.ts index bda90a4036..af4fec2efb 100644 --- a/src/features/stories/StoriesReducer.ts +++ b/src/features/stories/StoriesReducer.ts @@ -198,7 +198,7 @@ const oldStoriesReducer: Reducer = ( case UPDATE_STORIES_LIST: return { ...state, - storyList: action.payload + storyLists: action.payload }; case SET_CURRENT_STORY_ID: return { diff --git a/src/features/stories/StoriesTypes.ts b/src/features/stories/StoriesTypes.ts index 524210e5b2..84c3104235 100644 --- a/src/features/stories/StoriesTypes.ts +++ b/src/features/stories/StoriesTypes.ts @@ -35,10 +35,19 @@ export type StoryData = { title: string; content: string; pinOrder: number | null; + status: StoryStatus; + statusMessage: string; }; export type StoryParams = StoryData; +export enum StoryStatus { + Draft = 0, + Pending, + Rejected, + Published +} + export type StoryListView = StoryData & StoryMetadata & { id: number; @@ -69,8 +78,15 @@ export type StoriesAuthState = { readonly role?: StoriesRole; }; +export type StoryListViews = { + readonly draft: StoryListView[]; + readonly pending: StoryListView[]; + readonly rejected: StoryListView[]; + readonly published: StoryListView[]; +}; + export type StoriesState = { - readonly storyList: StoryListView[]; + readonly storyLists: StoryListViews; readonly currentStoryId: number | null; readonly currentStory: StoryData | null; readonly envs: { [key: string]: StoriesEnvState }; diff --git a/src/features/stories/storiesComponents/BackendAccess.ts b/src/features/stories/storiesComponents/BackendAccess.ts index f996240be4..f050a49469 100644 --- a/src/features/stories/storiesComponents/BackendAccess.ts +++ b/src/features/stories/storiesComponents/BackendAccess.ts @@ -11,7 +11,7 @@ import { store } from 'src/pages/createStore'; import { Tokens } from '../../../commons/application/types/SessionTypes'; import { NameUsernameRole } from '../../../pages/academy/adminPanel/subcomponents/AddStoriesUserPanel'; -import { StoryListView, StoryView } from '../StoriesTypes'; +import { StoryListView, StoryStatus, StoryView } from '../StoriesTypes'; // Helpers @@ -75,8 +75,22 @@ export const postNewStoriesUsers = async ( // TODO: Return response JSON directly. }; -export const getStories = async (tokens: Tokens): Promise => { - const resp = await requestStoryBackend(`/groups/${getStoriesGroupId()}/stories`, 'GET', { +export const getStories = async ( + tokens: Tokens, + status: StoryStatus | null = null +): Promise => { + const route = + status === StoryStatus.Draft + ? '/draft' + : status === StoryStatus.Pending + ? '/pending' + : status === StoryStatus.Rejected + ? '/rejected' + : status === StoryStatus.Published + ? '/published' + : ''; + + const resp = await requestStoryBackend(`/groups/${getStoriesGroupId()}/stories${route}`, 'GET', { ...tokens }); if (!resp) { @@ -124,10 +138,12 @@ export const updateStory = async ( id: number, title: string, content: string, - pinOrder: number | null + pinOrder: number | null, + status: StoryStatus, + statusMessage: string ): Promise => { const resp = await requestStoryBackend(`/groups/${getStoriesGroupId()}/stories/${id}`, 'PUT', { - body: { title, content, pinOrder }, + body: { title, content, pinOrder, status, statusMessage }, ...tokens }); if (!resp) { diff --git a/src/pages/stories/Stories.tsx b/src/pages/stories/Stories.tsx index 85357d9501..05bb5d181c 100644 --- a/src/pages/stories/Stories.tsx +++ b/src/pages/stories/Stories.tsx @@ -48,13 +48,13 @@ const Stories: React.FC = () => { [dispatch] ); - const storyList = useTypedSelector(state => state.stories.storyList); + const storyLists = useTypedSelector(state => state.stories.storyLists); const handleTogglePinStory = useCallback( (id: number) => { // Safe to use ! as the story ID comes a story in storyList - const story = storyList.find(story => story.id === id)!; - const pinnedLength = storyList.filter(story => story.isPinned).length; + const story = storyLists.published.find(story => story.id === id)!; + const pinnedLength = storyLists.published.filter(story => story.isPinned).length; const newStory = { ...story, isPinned: !story.isPinned, @@ -63,19 +63,19 @@ const Stories: React.FC = () => { }; dispatch(saveStory(newStory, id)); }, - [dispatch, storyList] + [dispatch, storyLists] ); const handleMovePinUp = useCallback( (id: number) => { // Safe to use ! as the story ID comes a story in storyList - const oldIndex = storyList.findIndex(story => story.id === id)!; + const oldIndex = storyLists.published.findIndex(story => story.id === id)!; if (oldIndex === 0) { return; } - const toMoveUp = storyList[oldIndex]; - const toMoveDown = storyList[oldIndex - 1]; + const toMoveUp = storyLists.published[oldIndex]; + const toMoveDown = storyLists.published[oldIndex - 1]; const storiesToUpdate = [ { ...toMoveUp, pinOrder: oldIndex - 1 }, @@ -83,19 +83,19 @@ const Stories: React.FC = () => { ]; storiesToUpdate.forEach(story => dispatch(saveStory(story, story.id))); }, - [dispatch, storyList] + [dispatch, storyLists] ); const handleMovePinDown = useCallback( (id: number) => { // Safe to use ! as the story ID comes a story in storyList - const oldIndex = storyList.findIndex(story => story.id === id)!; - const pinnedLength = storyList.filter(story => story.isPinned).length; + const oldIndex = storyLists.published.findIndex(story => story.id === id)!; + const pinnedLength = storyLists.published.filter(story => story.isPinned).length; if (oldIndex === pinnedLength - 1) { return; } - const toMoveDown = storyList[oldIndex]; - const toMoveUp = storyList[oldIndex + 1]; + const toMoveDown = storyLists.published[oldIndex]; + const toMoveUp = storyLists.published[oldIndex + 1]; const storiesToUpdate = [ { ...toMoveDown, pinOrder: oldIndex + 1 }, @@ -103,7 +103,7 @@ const Stories: React.FC = () => { ]; storiesToUpdate.forEach(story => dispatch(saveStory(story, story.id))); }, - [dispatch, storyList] + [dispatch, storyLists] ); return isStoriesDisabled ? ( @@ -140,7 +140,7 @@ const Stories: React.FC = () => { ({ ...story, content: getYamlHeader(story.content).content })) .filter( From 5afe3df54900450842b6d47bb55912fea5c07893 Mon Sep 17 00:00:00 2001 From: keyansheng <65121402+keyansheng@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:50:24 +0800 Subject: [PATCH 2/6] Add stories table title --- src/pages/stories/StoriesTable.tsx | 83 ++++++++++++++++-------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/src/pages/stories/StoriesTable.tsx b/src/pages/stories/StoriesTable.tsx index f0f88391eb..9466f02f26 100644 --- a/src/pages/stories/StoriesTable.tsx +++ b/src/pages/stories/StoriesTable.tsx @@ -9,7 +9,8 @@ import { TableHead, TableHeaderCell, TableRow, - Text + Text, + Title } from '@tremor/react'; import React from 'react'; import { StoryListView } from 'src/features/stories/StoriesTypes'; @@ -18,49 +19,53 @@ type Props = { headers: Array<{ id: string; header: string }>; stories: StoryListView[]; storyActions: (stor: StoryListView) => React.ReactNode; + title: string; }; const MAX_EXCERPT_LENGTH = 35; -const StoriesTable: React.FC = ({ headers, stories, storyActions }) => { +const StoriesTable: React.FC = ({ headers, stories, storyActions, title }) => { return ( - - - - {headers.map(({ id, header }) => ( - {header} - ))} - - - - {stories.map(story => { - const { id, authorName, isPinned, title, content } = story; - return ( - - {authorName} - - - {isPinned && } />} - {title} - - - - - {content.replaceAll(/\s+/g, ' ').length <= MAX_EXCERPT_LENGTH - ? content.replaceAll(/\s+/g, ' ') - : content.split(/\s+/).reduce((acc, cur) => { - return acc.length + cur.length <= MAX_EXCERPT_LENGTH - ? acc + ' ' + cur - : acc; - }, '') + '…'} - - - {storyActions(story)} - - ); - })} - -
+ <> + {title} + + + + {headers.map(({ id, header }) => ( + {header} + ))} + + + + {stories.map(story => { + const { id, authorName, isPinned, title, content } = story; + return ( + + {authorName} + + + {isPinned && } />} + {title} + + + + + {content.replaceAll(/\s+/g, ' ').length <= MAX_EXCERPT_LENGTH + ? content.replaceAll(/\s+/g, ' ') + : content.split(/\s+/).reduce((acc, cur) => { + return acc.length + cur.length <= MAX_EXCERPT_LENGTH + ? acc + ' ' + cur + : acc; + }, '') + '…'} + + + {storyActions(story)} + + ); + })} + +
+ ); }; From beafbfb046de40b9f3449f9f0003edb6096c6c1c Mon Sep 17 00:00:00 2001 From: keyansheng <65121402+keyansheng@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:21:27 +0800 Subject: [PATCH 3/6] Add reject and publish story actions --- src/pages/stories/StoryActions.tsx | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/pages/stories/StoryActions.tsx b/src/pages/stories/StoryActions.tsx index db5f1e020c..719dceb6fe 100644 --- a/src/pages/stories/StoryActions.tsx +++ b/src/pages/stories/StoryActions.tsx @@ -15,6 +15,10 @@ type Props = { handleTogglePin?: (id: number) => void; handleMovePinUp?: (id: number) => void; handleMovePinDown?: (id: number) => void; + canModerate?: boolean; + isPending?: boolean; + handleRejectStory?: (id: number) => void; + handlePublishStory?: (id: number) => void; }; const StoryActions: React.FC = ({ @@ -27,7 +31,11 @@ const StoryActions: React.FC = ({ isPinned = false, handleTogglePin = () => {}, handleMovePinUp = () => {}, - handleMovePinDown = () => {} + handleMovePinDown = () => {}, + canModerate = false, + isPending = false, + handleRejectStory = () => {}, + handlePublishStory = () => {} }) => { return ( @@ -91,6 +99,26 @@ const StoryActions: React.FC = ({ /> )} + {isPending && canModerate && ( + + )} + {isPending && canModerate && ( + + )} ); }; From 3901795374b124ed65a339eb4718b02b41c1eb3f Mon Sep 17 00:00:00 2001 From: keyansheng <65121402+keyansheng@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:11:59 +0800 Subject: [PATCH 4/6] Add handle reject and publish --- src/pages/stories/Stories.tsx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/pages/stories/Stories.tsx b/src/pages/stories/Stories.tsx index 05bb5d181c..3f5cc6e7ff 100644 --- a/src/pages/stories/Stories.tsx +++ b/src/pages/stories/Stories.tsx @@ -12,6 +12,7 @@ import { showSimpleConfirmDialog } from 'src/commons/utils/DialogHelper'; import { useTypedSelector } from 'src/commons/utils/Hooks'; import { deleteStory, getStoriesList, saveStory } from 'src/features/stories/StoriesActions'; import { getYamlHeader } from 'src/features/stories/storiesComponents/UserBlogContent'; +import { StoryStatus } from 'src/features/stories/StoriesTypes'; import StoriesTable from './StoriesTable'; import StoryActions from './StoryActions'; @@ -106,6 +107,32 @@ const Stories: React.FC = () => { [dispatch, storyLists] ); + const handleRejectStory = useCallback( + (id: number) => { + // Safe to use ! as the story ID comes a story in storyList + const story = storyLists.pending.find(story => story.id === id)!; + const newStory = { + ...story, + status: StoryStatus.Rejected + }; + dispatch(saveStory(newStory, id)); + }, + [dispatch, storyLists] + ); + + const handlePublishStory = useCallback( + (id: number) => { + // Safe to use ! as the story ID comes a story in storyList + const story = storyLists.pending.find(story => story.id === id)!; + const newStory = { + ...story, + status: StoryStatus.Published + }; + dispatch(saveStory(newStory, id)); + }, + [dispatch, storyLists] + ); + return isStoriesDisabled ? ( Date: Fri, 12 Apr 2024 09:53:20 +0800 Subject: [PATCH 5/6] Group stories by status --- src/pages/stories/Stories.tsx | 154 +++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 30 deletions(-) diff --git a/src/pages/stories/Stories.tsx b/src/pages/stories/Stories.tsx index 3f5cc6e7ff..58cc2b51f1 100644 --- a/src/pages/stories/Stories.tsx +++ b/src/pages/stories/Stories.tsx @@ -165,36 +165,130 @@ const Stories: React.FC = () => { /> - ({ ...story, content: getYamlHeader(story.content).content })) - .filter( - story => - // Always show pinned stories - story.isPinned || story.authorName.toLowerCase().includes(query.toLowerCase()) - )} - storyActions={story => { - const isAuthor = storiesUserId === story.authorId; - const hasWritePermissions = - storiesRole === StoriesRole.Moderator || storiesRole === StoriesRole.Admin; - return ( - - ); - }} - /> + {storyLists.published.length > 0 && ( + ({ ...story, content: getYamlHeader(story.content).content })) + .filter( + story => + // Always show pinned stories + story.isPinned || story.authorName.toLowerCase().includes(query.toLowerCase()) + )} + storyActions={story => { + const isAuthor = storiesUserId === story.authorId; + const hasWritePermissions = + storiesRole === StoriesRole.Moderator || storiesRole === StoriesRole.Admin; + return ( + + ); + }} + /> + )} + + {storyLists.pending.length > 0 && ( + ({ ...story, content: getYamlHeader(story.content).content })) + .filter( + story => + // Always show pinned stories + story.isPinned || story.authorName.toLowerCase().includes(query.toLowerCase()) + )} + storyActions={story => { + const isAuthor = storiesUserId === story.authorId; + const hasWritePermissions = + storiesRole === StoriesRole.Moderator || storiesRole === StoriesRole.Admin; + return ( + + ); + }} + /> + )} + + {storyLists.rejected.length > 0 && ( + ({ ...story, content: getYamlHeader(story.content).content })) + .filter( + story => + // Always show pinned stories + story.isPinned || story.authorName.toLowerCase().includes(query.toLowerCase()) + )} + storyActions={story => { + const isAuthor = storiesUserId === story.authorId; + const hasWritePermissions = + storiesRole === StoriesRole.Moderator || storiesRole === StoriesRole.Admin; + return ( + + ); + }} + /> + )} + + {storyLists.draft.length > 0 && ( + ({ ...story, content: getYamlHeader(story.content).content })) + .filter( + story => + // Always show pinned stories + story.isPinned || story.authorName.toLowerCase().includes(query.toLowerCase()) + )} + storyActions={story => { + const isAuthor = storiesUserId === story.authorId; + const hasWritePermissions = + storiesRole === StoriesRole.Moderator || storiesRole === StoriesRole.Admin; + return ( + + ); + }} + /> + )} } /> From 92a1fbd8c3c0f12a1dabec536f831cd61acf78d0 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Sun, 5 May 2024 22:04:29 +0800 Subject: [PATCH 6/6] Fix format post-merge --- src/commons/sagas/StoriesSaga.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commons/sagas/StoriesSaga.ts b/src/commons/sagas/StoriesSaga.ts index 2b273ce566..5c29bf0185 100644 --- a/src/commons/sagas/StoriesSaga.ts +++ b/src/commons/sagas/StoriesSaga.ts @@ -74,9 +74,8 @@ const StoriesSaga = combineSagaHandlers(StoriesActions, { pinOrder: null, status: StoryStatus.Draft, statusMessage: '' - }; - yield put(actions.setCurrentStory(defaultStory)); - + }; + yield put(actions.setCurrentStory(defaultStory)); } }, createStory: function* (action) {