* Run prettier on articleForm.jsx and tags.jsx The only thing this commit does is run prettier on files we'll be modifying to update the <Tags /> component. * Add basic snapshot test for ArticleForm I had to move algoliasearch setup to the constructor. Otherwise, the setup happens when you import ArticleForm. Feel free to let me know if there's a better way. * Add a snapshot test to exercise algoliasearch This test adds a mock for algoliasearch. We trigger handleTagKeyUp to test that tagList and tagOptions are updated once the mock search returns. * Add test for selecting tags There are two tests so far. 1. You can click on a tag to add it to the list. 2. You can type a comma which will also add the tag to the list. * Add constants for key codes * Change onKeyUp to onInput when typing tags I believe <Tags> is meant to be a controlled component. Using onKeyUp sort've acts as both uncontrolled and controlled. The value of the textarea has already been changed in the DOM, then we try to change it again using setState. This leads to some weird issues where what you type gets mangled. For example, you may type "javascript", but end up with something like "jvascrit". Switching this to onInput lets us control everything and gets rid of the weird issue of missing characters as you type. * Refactor the way we add tags to the input We add tags when you click on a search result or hit enter on a search result. I'm doing this because I think it will help insert tags at any location in the textarea. Currently, it only supports adding tags to the end of the list. * Allow editing of tag based on cursor position If you use the arrow keys or the mouse to go back to edit a tag you've typed, it'd be nice if the search results could replace that tag. Before this, if you click or hit enter on a search result, the tag gets added to the end of the list. This commit will replace the tag you're editing. * Move __mocks__ outside of __tests__ This was causing anything inside __mocks__ to be considered as a test. * Add 2 tests around editing tags 1. That we query for the correct tags based on what we're editing 2. That we replace the correct tag when we choose a new one * Enforce max tags limit * Refactor article form tests Extracts building of <ArticleForm /> component since we're using it in a number of places for test setup. * Add a space after inserting a tag There appears to be an issue with the input losing the cursor position when it rerenders. I'm not sure if this is an issue with the code or an issue with preact. I'm leaning toward an issue with the code, but I'm not really sure. * Refactor <Tags /> to class This is to prep for moving any tags related behavior from <ArticleForm /> to <Tags />. * Move tags behavior to <Tags /> component The <Tags /> component seems like the right place to perform searching and managing of selected tags. We can pass a function to <Tags /> so the tag list can be updated when needed. * Move <Tags /> selected state to a getter We already have `props.defaultValue` which is already a kind of "state" for selected tags. Keeping track of an array of selected tags seems like overkill here. We can transform the defaultValue string to a list of selected tags when needed. * Use linkState to manage tagList We're already using linkState elsewhere so I think it makes sense to use it here as well. * Extract key handling behavior to methods We were performing quite a bit of logic when handling key down events. This commit extracts this logic to small methods. For example, we need to check if we're at the top or bottom of the search results. We also need to clear search results and reset the selected search result. My hope is to make this logic a little bit clearer with methods named by what they do. * Move clearing of search results into search method If the query is blank, it seems to make sense that we'd clear search results inside the search method. That's an empty query and the search method is simply returning no search results in that case. However, if we don't wrap the resetting of search results with a Promise, we end up with a double render where the cursor moves to the end of the textarea. The Promise feels a little hacky to me, but I couldn't come up with something better. * Refactor naming of logic to insert a space following a comma This would've been hard to understand in the future. I named the conditionals a little bit better and extracted logic to insert a space at a given position. * Fix issue with duplicates appearing in search results This now uses the getter for selected tags as a filter.
232 lines
6.2 KiB
JavaScript
232 lines
6.2 KiB
JavaScript
import 'preact/devtools';
|
|
import { h, Component } from 'preact';
|
|
import linkState from 'linkstate';
|
|
import { submitArticle, previewArticle } from './actions';
|
|
import BodyMarkdown from './elements/bodyMarkdown';
|
|
import BodyPreview from './elements/bodyPreview';
|
|
import Description from './elements/description';
|
|
import PublishToggle from './elements/publishToggle';
|
|
import Notice from './elements/notice';
|
|
import Tags from './elements/tags';
|
|
import Title from './elements/title';
|
|
import MainImage from './elements/mainImage';
|
|
import ImageManagement from './elements/imageManagement';
|
|
import OrgSettings from './elements/orgSettings';
|
|
import Errors from './elements/errors';
|
|
import ImageUploadIcon from 'images/image-upload.svg';
|
|
import CodeMirror from 'codemirror';
|
|
import 'codemirror/mode/markdown/markdown';
|
|
|
|
export default class ArticleForm extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
const article = JSON.parse(this.props.article);
|
|
const organization = this.props.organization
|
|
? JSON.parse(this.props.organization)
|
|
: null;
|
|
this.state = {
|
|
id: article.id || null,
|
|
title: article.title || '',
|
|
tagList: article.cached_tag_list || '',
|
|
description: '',
|
|
bodyMarkdown: article.body_markdown || '',
|
|
published: article.published || false,
|
|
previewShowing: false,
|
|
helpShowing: false,
|
|
previewHTML: '',
|
|
helpHTML: document.getElementById('editor-help-guide').innerHTML,
|
|
submitting: false,
|
|
editing: article.id != null,
|
|
imageManagementShowing: false,
|
|
mainImageUrl: article.main_image || null,
|
|
organization,
|
|
postUnderOrg: false,
|
|
errors: null,
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
initEditorResize();
|
|
console.log('codemirror-ify');
|
|
const editor = document.getElementById('article_body_markdown');
|
|
const myCodeMirror = CodeMirror(editor, {
|
|
mode: 'markdown',
|
|
theme: 'material',
|
|
highlightFormatting: true,
|
|
});
|
|
myCodeMirror.setSize('100%', '100%');
|
|
}
|
|
|
|
toggleHelp = e => {
|
|
e.preventDefault();
|
|
window.scrollTo(0, 0);
|
|
this.setState({
|
|
helpShowing: !this.state.helpShowing,
|
|
previewShowing: false,
|
|
});
|
|
};
|
|
|
|
fetchPreview = e => {
|
|
e.preventDefault();
|
|
if (this.state.previewShowing) {
|
|
this.setState({
|
|
previewShowing: false,
|
|
helpShowing: false,
|
|
});
|
|
} else {
|
|
previewArticle(
|
|
this.state.bodyMarkdown,
|
|
this.showPreview,
|
|
this.failedPreview,
|
|
);
|
|
}
|
|
};
|
|
|
|
toggleImageManagement = e => {
|
|
e.preventDefault();
|
|
this.setState({
|
|
imageManagementShowing: !this.state.imageManagementShowing,
|
|
});
|
|
};
|
|
|
|
showPreview = response => {
|
|
this.setState({
|
|
previewShowing: true,
|
|
helpShowing: false,
|
|
previewHTML: response.processed_html,
|
|
});
|
|
};
|
|
|
|
toggleOrgPosting = e => {
|
|
e.preventDefault();
|
|
this.setState({ postUnderOrg: !this.state.postUnderOrg });
|
|
};
|
|
|
|
failedPreview = response => {
|
|
console.log(response);
|
|
};
|
|
|
|
handleMainImageUrlChange = payload => {
|
|
this.setState({
|
|
mainImageUrl: payload.link,
|
|
imageManagementShowing: false,
|
|
});
|
|
};
|
|
|
|
onPublish = e => {
|
|
e.preventDefault();
|
|
this.setState({ submitting: true, published: true });
|
|
const state = this.state;
|
|
state.published = true;
|
|
submitArticle(state, this.handleArticleError);
|
|
};
|
|
|
|
onSaveDraft = e => {
|
|
e.preventDefault();
|
|
this.setState({ submitting: true, published: false });
|
|
const state = this.state;
|
|
state.published = false;
|
|
submitArticle(state, this.handleArticleError);
|
|
};
|
|
|
|
handleArticleError = response => {
|
|
window.scrollTo(0, 0);
|
|
this.setState({
|
|
errors: response,
|
|
submitting: false,
|
|
});
|
|
};
|
|
|
|
render() {
|
|
// cover image url should asking for url OR providing option to upload an image
|
|
const {
|
|
title,
|
|
tagList,
|
|
description,
|
|
bodyMarkdown,
|
|
published,
|
|
previewShowing,
|
|
helpShowing,
|
|
previewHTML,
|
|
helpHTML,
|
|
submitting,
|
|
imageManagementShowing,
|
|
organization,
|
|
postUnderOrg,
|
|
mainImageUrl,
|
|
errors,
|
|
} = this.state;
|
|
// <input type="image" name="cover-image" />
|
|
|
|
let bodyArea = '';
|
|
if (previewShowing) {
|
|
bodyArea = <BodyPreview previewHTML={previewHTML} />;
|
|
} else if (helpShowing) {
|
|
bodyArea = <BodyPreview previewHTML={helpHTML} />;
|
|
} else {
|
|
bodyArea = (
|
|
<BodyMarkdown
|
|
defaultValue={bodyMarkdown}
|
|
onChange={linkState(this, 'bodyMarkdown')}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const notice = submitting ? <Notice published={published} /> : '';
|
|
const imageArea = mainImageUrl ? (
|
|
<MainImage mainImage={mainImageUrl} onEdit={this.toggleImageManagement} />
|
|
) : (
|
|
''
|
|
);
|
|
const imageManagement = imageManagementShowing ? (
|
|
<ImageManagement
|
|
onExit={this.toggleImageManagement}
|
|
mainImageUrl={mainImageUrl}
|
|
onMainImageUrlChange={this.handleMainImageUrlChange}
|
|
/>
|
|
) : (
|
|
''
|
|
);
|
|
const orgArea = organization ? (
|
|
<OrgSettings
|
|
organization={organization}
|
|
postUnderOrg={postUnderOrg}
|
|
onToggle={this.toggleOrgPosting}
|
|
/>
|
|
) : (
|
|
''
|
|
);
|
|
const errorsArea = errors ? <Errors errorsList={errors} /> : '';
|
|
return (
|
|
<form className="articleform__form" onSubmit={this.onSubmit}>
|
|
{errorsArea}
|
|
{orgArea}
|
|
{imageArea}
|
|
<Title defaultValue={title} onChange={linkState(this, 'title')} />
|
|
<div className="articleform__detailfields">
|
|
<Tags defaultValue={tagList} onInput={linkState(this, 'tagList')} />
|
|
<button
|
|
className="articleform__imageButton"
|
|
onClick={this.toggleImageManagement}
|
|
>
|
|
<img src={ImageUploadIcon} /> IMAGES
|
|
</button>
|
|
</div>
|
|
{bodyArea}
|
|
<PublishToggle
|
|
published={published}
|
|
previewShowing={previewShowing}
|
|
helpShowing={helpShowing}
|
|
onPreview={this.fetchPreview}
|
|
onPublish={this.onPublish}
|
|
onHelp={this.toggleHelp}
|
|
onSaveDraft={this.onSaveDraft}
|
|
onChange={linkState(this, 'published')}
|
|
/>
|
|
{notice}
|
|
{imageManagement}
|
|
</form>
|
|
);
|
|
}
|
|
}
|