Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Campaigns card in Multichannel Marketing page #36735

Merged
merged 45 commits into from Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
30d3168
Add empty state Campaigns card.
ecgan Dec 15, 2022
af4f79a
Add loading state for Campaigns card.
ecgan Dec 17, 2022
f5799c8
Display campaign data in table in Campaigns card.
ecgan Dec 18, 2022
c444dbd
CSS for campaign rows in Campaigns card.
ecgan Dec 18, 2022
32cf8d1
Add pagination to Campaigns card.
ecgan Dec 18, 2022
cbc616a
Set cost column as numeric in Campaigns card.
ecgan Dec 18, 2022
c67e479
Add Campaign type.
ecgan Dec 19, 2022
558ddf2
Use Campaign type in useCampaigns.
ecgan Dec 19, 2022
ee6fa95
Merge branch 'feature/34903-multichannel-marketing-frontend/34906-cha…
ecgan Dec 23, 2022
8e39098
Show Campaigns card only when there is at least one registered channel.
ecgan Dec 23, 2022
1d7b663
Remove the unneeded "Create new campaign" button for now.
ecgan Dec 23, 2022
7610214
Merge branch 'feature/34906-marketing-channels-card' into feature/349…
ecgan Jan 31, 2023
d69f7f0
Fix ids typescript error in Campaigns.
ecgan Jan 31, 2023
3384ad5
Remove unused import.
ecgan Jan 31, 2023
630bd0a
Merge branch 'feature/34906-marketing-channels-card' into feature/349…
ecgan Jan 31, 2023
cd7b5f2
Fetch and display campaigns with wp.data.
ecgan Feb 1, 2023
c342cbb
Change to color variable in Campaigns CSS.
ecgan Feb 1, 2023
2915442
Check description before rendering FlexItem.
ecgan Feb 1, 2023
274ea3d
Use Link component in Campaigns.
ecgan Feb 1, 2023
eda6f19
Use perPage constant instead of literal 5.
ecgan Feb 1, 2023
183fc48
Show CardFooter and Pagination only when it is needed.
ecgan Feb 1, 2023
743c0e5
Set per_page to 100 for getCampaigns.
ecgan Feb 1, 2023
0abe368
Display error state in Campaigns card.
ecgan Feb 2, 2023
c17f336
Code refactor for Campaigns card.
ecgan Feb 2, 2023
7b068bf
Code refactor for Campaigns card CSS.
ecgan Feb 2, 2023
048906a
Change Campaigns CSS to use @include font-size().
ecgan Feb 2, 2023
c9214ab
Merge branch 'feature/34906-marketing-channels-card' into feature/349…
ecgan Feb 2, 2023
20ae504
Add controls with fetchWithHeaders.
ecgan Feb 23, 2023
60d815c
wp.data for campaigns data pagination.
ecgan Feb 23, 2023
cc5345d
Campaigns pagination UI logic.
ecgan Feb 23, 2023
61aa3cd
Add changelog.
ecgan Feb 23, 2023
92762ca
Better pagination with TablePlaceholder in Campaigns card.
ecgan Feb 24, 2023
a982b7b
Error handling in getCampaigns.
ecgan Feb 24, 2023
f5ba6d9
Remove unused fetchWithHeaders control.
ecgan Feb 24, 2023
a4e1192
Change meta.total to optional in receiveCampaignsSuccess.
ecgan Feb 24, 2023
f7a1ef2
Code refactor with getTotalFromResponse in getCampaigns.
ecgan Feb 24, 2023
f62b44a
Set numberOfRows for TablePlaceholder in Campaigns card.
ecgan Feb 24, 2023
3513ef5
Merge branch 'trunk' into feature/34905-marketing-campaigns-card
ecgan Feb 24, 2023
e57181b
Add tests for Campaigns component.
ecgan Feb 26, 2023
8e9f344
Test code refactor with createTestCampaign.
ecgan Feb 28, 2023
b838885
Fix getTotalFromResponse treating total 0 as undefined.
ecgan Mar 1, 2023
5af90c5
Add JSDOC for getTotalFromResponse.
ecgan Mar 1, 2023
9feaf55
Simplify receive campaigns success and error.
ecgan Mar 1, 2023
4b87f0a
Add JSDOC.
ecgan Mar 1, 2023
c0364ad
Remove unused error property in CampaignsState.
ecgan Mar 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -7,4 +7,5 @@ export const TYPES = {
'RECEIVE_RECOMMENDED_CHANNELS_SUCCESS' as const,
RECEIVE_RECOMMENDED_CHANNELS_ERROR:
'RECEIVE_RECOMMENDED_CHANNELS_ERROR' as const,
RECEIVE_CAMPAIGNS: 'RECEIVE_CAMPAIGNS' as const,
};
Expand Up @@ -2,7 +2,12 @@
* Internal dependencies
*/
import { TYPES } from './action-types';
import { ApiFetchError, RegisteredChannel, RecommendedChannel } from './types';
import {
ApiFetchError,
RegisteredChannel,
RecommendedChannel,
Campaign,
} from './types';

export const receiveRegisteredChannelsSuccess = (
channels: Array< RegisteredChannel >
Expand Down Expand Up @@ -38,9 +43,42 @@ export const receiveRecommendedChannelsError = ( error: ApiFetchError ) => {
};
};

type CampaignsSuccessResponse = {
payload: Array< Campaign >;
error: false;
meta: {
page: number;
perPage: number;
total?: number;
};
};

type CampaignsFailResponse = {
payload: ApiFetchError;
error: true;
meta: {
page: number;
perPage: number;
total?: number;
};
};

type CampaignsResponse = CampaignsSuccessResponse | CampaignsFailResponse;

/**
* Create a "RECEIVE_CAMPAIGNS" action object.
*/
export const receiveCampaigns = ( response: CampaignsResponse ) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💅 Missed @param response docs

return {
type: TYPES.RECEIVE_CAMPAIGNS,
...response,
};
};

export type Action = ReturnType<
| typeof receiveRegisteredChannelsSuccess
| typeof receiveRegisteredChannelsError
| typeof receiveRecommendedChannelsSuccess
| typeof receiveRecommendedChannelsError
| typeof receiveCampaigns
>;
@@ -0,0 +1,23 @@
/**
* External dependencies
*/
import { controls as dataControls } from '@wordpress/data-controls';
import { AnyAction } from 'redux';

export const awaitResponseJson = (
response: Response
): AnyAction & { response: Response } => {
return {
type: 'AWAIT_RESPONSE_JSON',
response,
};
};

const controls = {
...dataControls,
AWAIT_RESPONSE_JSON( action: AnyAction ) {
return action.response.json();
},
};

export default controls;
Expand Up @@ -3,14 +3,14 @@
*/
import { createReduxStore, register } from '@wordpress/data';
import { Reducer, AnyAction } from 'redux';
import { controls } from '@wordpress/data-controls';

/**
* Internal dependencies
*/
import { State } from './types';
import { STORE_KEY } from './constants';
import { reducer } from './reducer';
import controls from './controls';
import * as actions from './actions';
import * as selectors from './selectors';
import * as resolvers from './resolvers';
Expand Down
Expand Up @@ -20,6 +20,11 @@ const initialState = {
data: undefined,
error: undefined,
},
campaigns: {
perPage: undefined,
pages: undefined,
total: undefined,
},
};

export const reducer: Reducer< State, Action > = (
Expand Down Expand Up @@ -55,6 +60,26 @@ export const reducer: Reducer< State, Action > = (
error: action.payload,
},
};

case TYPES.RECEIVE_CAMPAIGNS:
return {
...state,
campaigns: {
perPage: action.meta.perPage,
pages: {
...state.campaigns.pages,
[ action.meta.page ]: action.error
? {
error: action.payload,
}
: {
data: action.payload,
},
},
total: action.meta.total,
},
};

default:
return state;
}
Expand Down
Expand Up @@ -11,8 +11,15 @@ import {
receiveRegisteredChannelsError,
receiveRecommendedChannelsSuccess,
receiveRecommendedChannelsError,
receiveCampaigns,
} from './actions';
import { RegisteredChannel, RecommendedChannel } from './types';
import { awaitResponseJson } from './controls';
import {
RegisteredChannel,
RecommendedChannel,
Campaign,
ApiFetchError,
} from './types';
import { API_NAMESPACE } from './constants';
import { isApiFetchError } from './guards';

Expand Down Expand Up @@ -47,3 +54,63 @@ export function* getRecommendedChannels() {
throw error;
}
}

/**
* Get total number of records from the HTTP response header "x-wp-total".
*
* If the header is not present, then the function will return `undefined`.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed param response docs

const getTotalFromResponse = ( response: Response ) => {
ecgan marked this conversation as resolved.
Show resolved Hide resolved
const total = response.headers.get( 'x-wp-total' );

if ( total === null ) {
return undefined;
}

return parseInt( total, 10 );
};

/**
* Get campaigns from API backend.
*
* @param page Page number. First page is `1`.
* @param perPage Page size, i.e. number of records in one page.
*/
export function* getCampaigns( page: number, perPage: number ) {
puntope marked this conversation as resolved.
Show resolved Hide resolved
try {
const response: Response = yield apiFetch( {
path: `${ API_NAMESPACE }/campaigns?page=${ page }&per_page=${ perPage }`,
parse: false,
} );

const total = getTotalFromResponse( response );
const payload: Campaign[] = yield awaitResponseJson( response );

yield receiveCampaigns( {
payload,
error: false,
meta: {
page,
perPage,
total,
},
} );
} catch ( error ) {
if ( error instanceof Response ) {
const total = getTotalFromResponse( error );
const payload: ApiFetchError = yield awaitResponseJson( error );

yield receiveCampaigns( {
payload,
error: true,
meta: {
page,
perPage,
total,
},
} );
}

throw error;
}
}
Expand Up @@ -10,3 +10,10 @@ export const getRegisteredChannels = ( state: State ) => {
export const getRecommendedChannels = ( state: State ) => {
return state.recommendedChannels;
};

/**
* Get campaigns from state.
*/
export const getCampaigns = ( state: State ) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💅 Missed JSDOCS

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4b87f0a (#36735).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed @param state declaration

return state.campaigns;
};
Expand Up @@ -50,7 +50,30 @@ export type RecommendedChannelsState = {
error?: ApiFetchError;
};

export type Campaign = {
id: string;
channel: string;
title: string;
manage_url: string;
cost: {
value: string;
currency: string;
};
};

export type CampaignsPage = {
data?: Array< Campaign >;
error?: ApiFetchError;
};

export type CampaignsState = {
perPage?: number;
pages?: Record< number, CampaignsPage >;
total?: number;
};

export type State = {
registeredChannels: RegisteredChannelsState;
recommendedChannels: RecommendedChannelsState;
campaigns: CampaignsState;
};
@@ -0,0 +1,59 @@
.woocommerce-marketing-campaigns-card {
&__content {
width: 50%;
margin: auto;
text-align: center;
}

&__content-icon {
background-color: $studio-gray-0;
border-radius: 28px;
margin-bottom: $gap-smallest;

&--error {
fill: $alert-red;
}

&--empty {
fill: $studio-woocommerce-purple-50;
}
}

&__content-title {
@include font-size( 13 );
font-weight: 600;
line-height: 16px;
color: $gray-900;
margin-bottom: $gap-smallest;
}

&__content-description {
color: $gray-700;
margin-bottom: $gap-smallest;
}

&__campaign-logo {
img {
display: block;
}
}

&__campaign-title {
a {
color: #007cba;
font-weight: 600;
text-decoration: none;
}
}

&__campaign-description {
color: $gray-700;
}

&__footer {
justify-content: center;

// Remove the border-top because the table cells already has a border-bottom.
border-top: none;
}
}