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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable creating reader lists #43164

Merged
merged 13 commits into from Jun 12, 2020
214 changes: 149 additions & 65 deletions client/reader/list-manage/index.jsx
Expand Up @@ -2,14 +2,18 @@
* External dependencies
*/
import * as React from 'react';
import { connect } from 'react-redux';
import { localize } from 'i18n-calypso';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslate } from 'i18n-calypso';

/**
* Internal dependencies
*/
import { Button, Card } from '@automattic/components';
import { getListByOwnerAndSlug, getListItems } from 'state/reader/lists/selectors';
import {
getListByOwnerAndSlug,
getListItems,
isCreatingList as isCreatingListSelector,
} from 'state/reader/lists/selectors';
import FormattedHeader from 'components/formatted-header';
import FormButtonsBar from 'components/forms/form-buttons-bar';
import FormButton from 'components/forms/form-button';
Expand All @@ -27,23 +31,153 @@ import SectionNav from 'components/section-nav';
import NavTabs from 'components/section-nav/tabs';
import NavItem from 'components/section-nav/item';
import Main from 'components/main';
import { createReaderList } from 'state/reader/lists/actions';
import ReaderExportButton from 'blocks/reader-export-button';
import ListItem from './list-item';
import { READER_EXPORT_TYPE_LIST } from 'blocks/reader-export-button/constants';
import ListItem from './list-item';

/**
* Style dependencies
*/
import './style.scss';

function ListForm( { isCreateForm, isSubmissionDisabled, list, onChange, onSubmit } ) {
const translate = useTranslate();
const isNameValid = typeof list.title === 'string' && list.title.length > 0;
const isSlugValid = isCreateForm || ( typeof list.slug === 'string' && list.slug.length > 0 );
return (
<Card>
<FormFieldset>
<FormLabel htmlFor="list-name">{ translate( 'Name (Required)' ) }</FormLabel>
<FormTextInput
data-key="title"
id="list-name"
isValid={ isNameValid }
name="list-name"
onChange={ onChange }
value={ list.title }
/>
<FormSettingExplanation>{ translate( 'The name of the list.' ) }</FormSettingExplanation>
</FormFieldset>

{ ! isCreateForm && (
<FormFieldset>
<FormLabel htmlFor="list-slug">{ translate( 'Slug (Required)' ) }</FormLabel>
<FormTextInput
data-key="slug"
id="list-slug"
isValid={ isSlugValid }
name="list-slug"
onChange={ onChange }
value={ list.slug }
/>
<FormSettingExplanation>
{ translate( 'The slug for the list. This is used to build the URL to the list.' ) }
</FormSettingExplanation>
</FormFieldset>
) }

<FormFieldset>
<FormLegend>{ translate( 'Visibility' ) }</FormLegend>
<FormLabel>
<FormRadio
checked={ list.is_public }
data-key="is_public"
onChange={ onChange }
value="public"
/>
<span>{ translate( 'Everyone can view this list' ) }</span>
</FormLabel>

<FormLabel>
<FormRadio
checked={ ! list.is_public }
data-key="is_public"
onChange={ onChange }
value="private"
/>
<span>{ translate( 'Only I can view this list' ) }</span>
</FormLabel>
<FormSettingExplanation>
{ translate(
"Don't worry, posts from private sites will only appear to those with access. " +
'Adding a private site to a public list will not make posts from that site accessible to everyone.'
) }
</FormSettingExplanation>
</FormFieldset>

<FormFieldset>
<FormLabel htmlFor="list-description">{ translate( 'Description' ) }</FormLabel>
<FormTextarea
data-key="description"
id="list-description"
name="list-description"
onChange={ onChange }
placeholder={ translate( "What's your list about?" ) }
value={ list.description }
/>
</FormFieldset>
<FormButtonsBar>
<FormButton
primary
disabled={ isSubmissionDisabled || ! isNameValid || ! isSlugValid }
onClick={ onSubmit }
>
{ translate( 'Save' ) }
</FormButton>
</FormButtonsBar>
</Card>
);
}

function ReaderListCreate() {
const translate = useTranslate();
const dispatch = useDispatch();
const isCreatingList = useSelector( isCreatingListSelector );
const [ list, updateList ] = React.useState( {
description: '',
is_public: true,
slug: '',
title: '',
} );
const onChange = ( event ) => {
const update = { [ event.target.dataset.key ]: event.target.value };
if ( 'is_public' in update ) {
update.is_public = update.is_public === 'public';
}
updateList( { ...list, ...update } );
};
return (
<Main>
<FormattedHeader headerText={ translate( 'Create List' ) } />
<ListForm
isCreateForm
isSubmissionDisabled={ isCreatingList }
list={ list }
onChange={ onChange }
onSubmit={ () => dispatch( createReaderList( list ) ) }
/>
</Main>
);
}

function ReaderListEdit( props ) {
const { list, listItems, selectedSection, translate } = props;
const { selectedSection } = props;
const translate = useTranslate();
const list = useSelector( ( state ) => getListByOwnerAndSlug( state, props.owner, props.slug ) );
const listItems = useSelector( ( state ) =>
list ? getListItems( state, list.ID ) : undefined
);
return (
<>
{ ! list && <QueryReaderList owner={ props.owner } slug={ props.slug } /> }
{ ! listItems && list && <QueryReaderListItems owner={ props.owner } slug={ props.slug } /> }
<Main>
<FormattedHeader headerText={ `Manage ${ list?.title || props.slug }` } />
<FormattedHeader
headerText={ translate( 'Manage %(listName)s', {
args: { listName: list?.title || props.slug },
} ) }
/>
{ ! list && <Card>Loading...</Card> }
{ list && (
<>
Expand Down Expand Up @@ -75,54 +209,7 @@ function ReaderListEdit( props ) {
</SectionNav>
{ selectedSection === 'details' && (
<>
<Card>
<FormSectionHeading>List Details</FormSectionHeading>

<FormFieldset>
<FormLabel htmlFor="list-name">Name</FormLabel>
<FormTextInput id="list-name" name="list-name" value={ list.title } />
<FormSettingExplanation>The name of the list.</FormSettingExplanation>
</FormFieldset>

<FormFieldset>
<FormLabel htmlFor="list-slug">Slug</FormLabel>
<FormTextInput id="list-slug" name="list-slug" value={ list.slug } />
<FormSettingExplanation>
The slug for the list. This is used to build the URL to the list.
</FormSettingExplanation>
</FormFieldset>

<FormFieldset>
<FormLegend>Visibility</FormLegend>
<FormLabel>
<FormRadio value="public" checked={ list.is_public } />
<span>Everyone can view this list</span>
</FormLabel>

<FormLabel>
<FormRadio value="private" checked={ ! list.is_public } />
<span>Only I can view this list</span>
</FormLabel>
<FormSettingExplanation>
Don't worry, posts from private sites will only appear to those with access.
Adding a private site to a public list will not make posts from that site
accessible to everyone.
</FormSettingExplanation>
</FormFieldset>

<FormFieldset>
<FormLabel htmlFor="list-description">Description</FormLabel>
<FormTextarea
name="list-description"
id="list-description"
placeholder="What's your list about?"
value={ list.description }
/>
</FormFieldset>
<FormButtonsBar>
<FormButton primary>Save</FormButton>
</FormButtonsBar>
</Card>
<ListForm list={ list } />

<Card>
<FormSectionHeading>DANGER!!</FormSectionHeading>
Expand All @@ -133,14 +220,16 @@ function ReaderListEdit( props ) {
</>
) }
{ selectedSection === 'items' &&
props.listItems?.map( ( item ) => (
listItems?.map( ( item ) => (
<ListItem key={ item.ID } owner={ props.owner } list={ list } item={ item } />
) ) }

{ selectedSection === 'export' && (
<Card>
<p>
You can export this list to use on other services. The file will be in OPML
format.
{ translate(
'You can export this list to use on other services. The file will be in OPML format.'
) }
</p>
<ReaderExportButton exportType={ READER_EXPORT_TYPE_LIST } listId={ list.ID } />
</Card>
Expand All @@ -152,11 +241,6 @@ function ReaderListEdit( props ) {
);
}

export default connect( ( state, ownProps ) => {
const list = getListByOwnerAndSlug( state, ownProps.owner, ownProps.slug );
const listItems = list ? getListItems( state, list.ID ) : undefined;
return {
list,
listItems,
};
} )( localize( ReaderListEdit ) );
export default function ReaderListManage( props ) {
return props.isCreateForm ? ReaderListCreate() : ReaderListEdit( props );
}
9 changes: 9 additions & 0 deletions client/reader/list-manage/types.d.ts
@@ -1,3 +1,12 @@
export type List = {
ID: number;
description: string;
is_public: boolean;
is_owner: boolean;
slug: string;
title: string;
};

export type Item = {
id: string;
feed_ID: number | null;
Expand Down
12 changes: 12 additions & 0 deletions client/reader/list/controller.js
Expand Up @@ -12,6 +12,18 @@ import AsyncLoad from 'components/async-load';

const analyticsPageTitle = 'Reader';

export const createList = ( context, next ) => {
const basePath = '/read/list/new';
const fullAnalyticsPageTitle = `${ analyticsPageTitle } > List > Create`;
const mcKey = 'list';

trackPageLoad( basePath, fullAnalyticsPageTitle, mcKey );
recordTrack( 'calypso_reader_list_create_loaded' );

context.primary = <AsyncLoad require="reader/list-manage" key="list-manage" isCreateForm />;
next();
};

export const listListing = ( context, next ) => {
const basePath = '/read/list/:owner/:slug';
const fullAnalyticsPageTitle =
Expand Down
6 changes: 5 additions & 1 deletion client/reader/list/index.js
Expand Up @@ -6,7 +6,7 @@ import page from 'page';
/**
* Internal dependencies
*/
import { editList, editListItems, exportList, listListing } from './controller';
import { createList, editList, editListItems, exportList, listListing } from './controller';
import { sidebar, updateLastRoute } from 'reader/controller';
import { makeLayout, render as clientRender } from 'controller';

Expand All @@ -27,6 +27,9 @@ export default function () {
makeLayout,
clientRender
);

page( '/read/list/new', updateLastRoute, sidebar, createList, makeLayout, clientRender );

page(
'/read/list/:user/:list/export',
updateLastRoute,
Expand All @@ -35,5 +38,6 @@ export default function () {
makeLayout,
clientRender
);

page( '/read/list/:user/:list', updateLastRoute, sidebar, listListing, makeLayout, clientRender );
}
39 changes: 39 additions & 0 deletions client/state/data-layer/wpcom/read/lists/index.js
@@ -0,0 +1,39 @@
/**
* Internal dependencies
*/
import { http } from 'state/data-layer/wpcom-http/actions';
import { dispatchRequest } from 'state/data-layer/wpcom-http/utils';
import { errorNotice } from 'state/notices/actions';
import { READER_LIST_CREATE } from 'state/reader/action-types';
import { receiveReaderList, handleReaderListRequestFailure } from 'state/reader/lists/actions';
import { registerHandlers } from 'state/data-layer/handler-registry';
import { navigate } from 'state/ui/actions';

registerHandlers( 'state/data-layer/wpcom/read/lists/index.js', {
[ READER_LIST_CREATE ]: [
dispatchRequest( {
fetch: ( action ) =>
http(
{
method: 'POST',
path: `/read/lists/new`,
apiVersion: '1.2',
body: {
title: action.list.title,
description: action.list.description,
is_public: action.list.is_public,
},
},
action
),
onSuccess: ( action, response ) => [
receiveReaderList( { list: response.list } ),
navigate( `/read/list/${ response.list.owner }/${ response.list.slug }/edit` ),
],
onError: ( action, error ) => [
errorNotice( String( error ) ),
handleReaderListRequestFailure( error ),
],
} ),
],
} );
1 change: 1 addition & 0 deletions client/state/reader/action-types.js
Expand Up @@ -45,6 +45,7 @@ export const READER_LISTS_REQUEST_SUCCESS = 'READER_LISTS_REQUEST_SUCCESS';
export const READER_LISTS_UNFOLLOW = 'READER_LISTS_UNFOLLOW';
export const READER_LISTS_UNFOLLOW_FAILURE = 'READER_LISTS_UNFOLLOW_FAILURE';
export const READER_LISTS_UNFOLLOW_SUCCESS = 'READER_LISTS_UNFOLLOW_SUCCESS';
export const READER_LIST_CREATE = 'READER_LIST_CREATE';
export const READER_LIST_DISMISS_NOTICE = 'READER_LIST_DISMISS_NOTICE';
export const READER_LIST_REQUEST = 'READER_LIST_REQUEST';
export const READER_LIST_REQUEST_FAILURE = 'READER_LIST_REQUEST_FAILURE';
Expand Down