339 lines
12 KiB
React
339 lines
12 KiB
React
|
// @flow
|
||
|
import React from 'react';
|
||
|
import Button from 'component/button';
|
||
|
import TagsSearch from 'component/tagsSearch';
|
||
|
import ErrorText from 'component/common/error-text';
|
||
|
import ClaimAbandonButton from 'component/claimAbandonButton';
|
||
|
import CollectionItemsList from 'component/collectionItemsList';
|
||
|
import Card from 'component/common/card';
|
||
|
import { useHistory } from 'react-router-dom';
|
||
|
import { isNameValid } from 'util/lbryURI';
|
||
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||
|
import { FormField } from 'component/common/form';
|
||
|
import { handleBidChange, handleLanguageChange } from 'util/publish';
|
||
|
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||
|
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
||
|
import * as PAGES from 'constants/pages';
|
||
|
import * as PUBLISH from 'constants/publish';
|
||
|
import * as COLLECTIONS_CONSTS from 'constants/collections';
|
||
|
import analytics from 'analytics';
|
||
|
import CollectionGeneralTab from 'component/collectionGeneralTab';
|
||
|
import PublishBidTab from 'component/publishBidField';
|
||
|
import Spinner from 'component/spinner';
|
||
|
|
||
|
export const PAGE_TAB_QUERY = `tab`;
|
||
|
const MAX_TAG_SELECT = 5;
|
||
|
|
||
|
const TAB = {
|
||
|
GENERAL: 0,
|
||
|
ITEMS: 1,
|
||
|
CREDITS: 2,
|
||
|
TAGS: 3,
|
||
|
OTHER: 4,
|
||
|
};
|
||
|
|
||
|
type Props = {
|
||
|
uri: string, // collection uri
|
||
|
collectionId: string,
|
||
|
// -- redux -
|
||
|
hasClaim: boolean,
|
||
|
balance: number,
|
||
|
amount: number,
|
||
|
collectionParams: CollectionPublishParams,
|
||
|
collectionClaimIds: Array<string>,
|
||
|
updatingCollection: boolean,
|
||
|
updateError: string,
|
||
|
createError: string,
|
||
|
creatingCollection: boolean,
|
||
|
doCollectionPublishUpdate: (CollectionUpdateParams) => Promise<any>,
|
||
|
doCollectionPublish: (CollectionPublishParams, string) => Promise<any>,
|
||
|
doClearCollectionErrors: () => void,
|
||
|
onDone: (string) => void,
|
||
|
};
|
||
|
|
||
|
function CollectionForm(props: Props) {
|
||
|
const {
|
||
|
uri,
|
||
|
collectionId,
|
||
|
// -- redux -
|
||
|
hasClaim,
|
||
|
balance,
|
||
|
amount,
|
||
|
collectionParams,
|
||
|
collectionClaimIds,
|
||
|
updatingCollection,
|
||
|
updateError,
|
||
|
createError,
|
||
|
creatingCollection,
|
||
|
doCollectionPublishUpdate,
|
||
|
doCollectionPublish,
|
||
|
doClearCollectionErrors,
|
||
|
onDone,
|
||
|
} = props;
|
||
|
|
||
|
const { replace } = useHistory();
|
||
|
|
||
|
const [nameError, setNameError] = React.useState(undefined);
|
||
|
const [thumbailError, setThumbnailError] = React.useState('');
|
||
|
const [bidError, setBidError] = React.useState('');
|
||
|
const [params, setParams] = React.useState({});
|
||
|
const [loading, setLoading] = React.useState(false);
|
||
|
const [tabIndex, setTabIndex] = React.useState(0);
|
||
|
const [showItemsSpinner, setShowItemsSpinner] = React.useState(false);
|
||
|
|
||
|
const { name, languages, claims, tags } = params;
|
||
|
|
||
|
const isBuiltin = COLLECTIONS_CONSTS.BUILTIN_PLAYLISTS.includes(collectionId);
|
||
|
const isNewCollection = !uri;
|
||
|
const languageParam = languages || [];
|
||
|
const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0];
|
||
|
const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1];
|
||
|
const hasClaims = claims && claims.length;
|
||
|
const collectionClaimIdsString = JSON.stringify(collectionClaimIds);
|
||
|
const itemError = !hasClaims ? __('Cannot publish empty list') : '';
|
||
|
const submitError = nameError || bidError || itemError || updateError || createError || thumbailError;
|
||
|
|
||
|
function updateParams(newParams) {
|
||
|
// $FlowFixMe
|
||
|
setParams({ ...params, ...newParams });
|
||
|
}
|
||
|
|
||
|
function handleSubmit() {
|
||
|
if (uri) {
|
||
|
doCollectionPublishUpdate(params).then((pendingClaim) => {
|
||
|
if (pendingClaim) {
|
||
|
const claimId = pendingClaim.claim_id;
|
||
|
analytics.apiLogPublish(pendingClaim);
|
||
|
onDone(claimId);
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
// $FlowFixMe
|
||
|
doCollectionPublish(params, collectionId).then((pendingClaim) => {
|
||
|
if (pendingClaim) {
|
||
|
const claimId = pendingClaim.claim_id;
|
||
|
analytics.apiLogPublish(pendingClaim);
|
||
|
onDone(claimId);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
React.useEffect(() => {
|
||
|
const collectionClaimIds = JSON.parse(collectionClaimIdsString);
|
||
|
// $FlowFixMe
|
||
|
updateParams({ claims: collectionClaimIds });
|
||
|
doClearCollectionErrors();
|
||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
}, [collectionClaimIdsString]);
|
||
|
|
||
|
React.useEffect(() => {
|
||
|
let nameError;
|
||
|
if (!name && name !== undefined) {
|
||
|
nameError = __('A name is required for your url');
|
||
|
} else if (!isNameValid(name)) {
|
||
|
nameError = INVALID_NAME_ERROR;
|
||
|
}
|
||
|
|
||
|
setNameError(nameError);
|
||
|
}, [name]);
|
||
|
|
||
|
// setup initial params after we're sure if it's published or not
|
||
|
React.useEffect(() => {
|
||
|
if (!uri || (uri && hasClaim)) {
|
||
|
updateParams(collectionParams);
|
||
|
}
|
||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
}, [uri, hasClaim]);
|
||
|
|
||
|
function onTabChange(newTabIndex) {
|
||
|
if (tabIndex !== newTabIndex) {
|
||
|
if (newTabIndex === TAB.ITEMS) {
|
||
|
setShowItemsSpinner(true);
|
||
|
setTimeout(() => {
|
||
|
// Wait enough time for the spinner to appear, then switch tabs.
|
||
|
setTabIndex(newTabIndex);
|
||
|
// We can stop the spinner immediately. If the list takes a long time
|
||
|
// to render, the spinner would continue to spin until the
|
||
|
// state-change is flushed.
|
||
|
setShowItemsSpinner(false);
|
||
|
}, 250);
|
||
|
} else {
|
||
|
setTabIndex(newTabIndex);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
<div className="main--contained publishList-wrapper">
|
||
|
<Tabs onChange={onTabChange} index={tabIndex}>
|
||
|
<TabList className="tabs__list--collection-edit-page">
|
||
|
<Tab>{__('General')}</Tab>
|
||
|
<Tab>{__('Items')}</Tab>
|
||
|
<Tab>{__('Credits')}</Tab>
|
||
|
<Tab>{__('Tags')}</Tab>
|
||
|
<Tab>
|
||
|
{__('Other')}
|
||
|
{showItemsSpinner && <Spinner type="small" />}
|
||
|
</Tab>
|
||
|
</TabList>
|
||
|
|
||
|
<TabPanels>
|
||
|
<TabPanel>
|
||
|
{tabIndex === TAB.GENERAL && (
|
||
|
<CollectionGeneralTab
|
||
|
uri={uri}
|
||
|
params={params}
|
||
|
nameError={nameError}
|
||
|
setThumbnailError={setThumbnailError}
|
||
|
updateParams={updateParams}
|
||
|
setLoading={setLoading}
|
||
|
/>
|
||
|
)}
|
||
|
</TabPanel>
|
||
|
|
||
|
<TabPanel>
|
||
|
{tabIndex === TAB.ITEMS && (
|
||
|
<CollectionItemsList collectionId={collectionId} empty={__('This playlist has no items.')} showEdit />
|
||
|
)}
|
||
|
</TabPanel>
|
||
|
|
||
|
<TabPanel>
|
||
|
{tabIndex === TAB.CREDITS && (
|
||
|
<PublishBidTab
|
||
|
params={params}
|
||
|
bidError={bidError}
|
||
|
onChange={(event) =>
|
||
|
handleBidChange(parseFloat(event.target.value), amount, balance, setBidError, updateParams)
|
||
|
}
|
||
|
/>
|
||
|
)}
|
||
|
</TabPanel>
|
||
|
|
||
|
<TabPanel>
|
||
|
{tabIndex === TAB.TAGS && (
|
||
|
<Card
|
||
|
body={
|
||
|
<TagsSearch
|
||
|
suggestMature
|
||
|
disableAutoFocus
|
||
|
limitSelect={MAX_TAG_SELECT}
|
||
|
tagsPassedIn={tags || []}
|
||
|
label={__('Selected Tags')}
|
||
|
onRemove={(clickedTag) => {
|
||
|
// $FlowFixMe
|
||
|
const newTags = tags.slice().filter((tag) => tag.name !== clickedTag.name);
|
||
|
updateParams({ tags: newTags });
|
||
|
}}
|
||
|
onSelect={(newTags) => {
|
||
|
tags &&
|
||
|
newTags.forEach((newTag) => {
|
||
|
// $FlowFixMe
|
||
|
if (!tags.map((savedTag) => savedTag.name).includes(newTag.name)) {
|
||
|
// $FlowFixMe
|
||
|
updateParams({ tags: [...tags, newTag] });
|
||
|
} else {
|
||
|
// If it already exists and the user types it in, remove itit
|
||
|
// $FlowFixMe
|
||
|
updateParams({ tags: tags.filter((tag) => tag.name !== newTag.name) });
|
||
|
}
|
||
|
});
|
||
|
}}
|
||
|
/>
|
||
|
}
|
||
|
/>
|
||
|
)}
|
||
|
</TabPanel>
|
||
|
|
||
|
<TabPanel>
|
||
|
{tabIndex === TAB.OTHER && (
|
||
|
<Card
|
||
|
body={
|
||
|
<>
|
||
|
<FormField
|
||
|
name="language_select"
|
||
|
type="select"
|
||
|
label={__('Primary Language')}
|
||
|
onChange={(event) =>
|
||
|
handleLanguageChange(0, event.target.value, languageParam, setParams, params)
|
||
|
}
|
||
|
value={primaryLanguage}
|
||
|
helper={__('Your main content language')}
|
||
|
>
|
||
|
<option key={'pri-langNone'} value={PUBLISH.LANG_NONE}>
|
||
|
{__('None selected')}
|
||
|
</option>
|
||
|
{Object.keys(SUPPORTED_LANGUAGES).map((language) => (
|
||
|
<option key={language} value={language}>
|
||
|
{SUPPORTED_LANGUAGES[language]}
|
||
|
</option>
|
||
|
))}
|
||
|
</FormField>
|
||
|
<FormField
|
||
|
name="language_select2"
|
||
|
type="select"
|
||
|
label={__('Secondary Language')}
|
||
|
onChange={(event) =>
|
||
|
handleLanguageChange(1, event.target.value, languageParam, setParams, params)
|
||
|
}
|
||
|
value={secondaryLanguage}
|
||
|
disabled={!languageParam[0]}
|
||
|
helper={__('Your other content language')}
|
||
|
>
|
||
|
<option key={'sec-langNone'} value={PUBLISH.LANG_NONE}>
|
||
|
{__('None selected')}
|
||
|
</option>
|
||
|
{Object.keys(SUPPORTED_LANGUAGES)
|
||
|
.filter((lang) => lang !== languageParam[0])
|
||
|
.map((language) => (
|
||
|
<option key={language} value={language}>
|
||
|
{SUPPORTED_LANGUAGES[language]}
|
||
|
</option>
|
||
|
))}
|
||
|
</FormField>
|
||
|
</>
|
||
|
}
|
||
|
/>
|
||
|
)}
|
||
|
</TabPanel>
|
||
|
</TabPanels>
|
||
|
</Tabs>
|
||
|
|
||
|
{!loading && (
|
||
|
<Card
|
||
|
className="card--after-tabs"
|
||
|
actions={
|
||
|
<>
|
||
|
<div className="section__actions">
|
||
|
<Button
|
||
|
button="primary"
|
||
|
disabled={isBuiltin || creatingCollection || updatingCollection || Boolean(submitError) || !hasClaims}
|
||
|
label={creatingCollection || updatingCollection ? __('Submitting') : __('Submit')}
|
||
|
onClick={handleSubmit}
|
||
|
/>
|
||
|
<Button button="link" label={__('Cancel')} onClick={() => onDone(collectionId)} />
|
||
|
</div>
|
||
|
|
||
|
{submitError || isBuiltin ? (
|
||
|
<ErrorText>{submitError || (isBuiltin && __("Can't publish default playlists."))}</ErrorText>
|
||
|
) : (
|
||
|
<p className="help">
|
||
|
{__('After submitting, it will take a few minutes for your changes to be live for everyone.')}
|
||
|
</p>
|
||
|
)}
|
||
|
|
||
|
{!isNewCollection && (
|
||
|
<div className="section__actions">
|
||
|
<ClaimAbandonButton uri={uri} abandonActionCallback={() => replace(`/$/${PAGES.LIBRARY}`)} />
|
||
|
</div>
|
||
|
)}
|
||
|
</>
|
||
|
}
|
||
|
/>
|
||
|
)}
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
export default CollectionForm;
|