EditorSettings.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import React, {
  2. Dispatch, memo,
  3. FC, SetStateAction, useCallback, useEffect, useState,
  4. useMemo,
  5. } from 'react';
  6. import { useTranslation } from 'next-i18next';
  7. import { toastSuccess, toastError } from '~/client/util/toastr';
  8. import { useEditorSettings } from '~/stores/editor';
  9. type EditorSettingsBodyProps = Record<string, never>;
  10. type RuleListGroupProps = {
  11. title: string;
  12. ruleList: RulesMenuItem[]
  13. textlintRules: LintRule[]
  14. setTextlintRules: Dispatch<SetStateAction<LintRule[]>>
  15. }
  16. type LintRule = {
  17. name: string
  18. options?: unknown
  19. isEnabled?: boolean
  20. }
  21. type RulesMenuItem = {
  22. name: string
  23. description: string
  24. }
  25. const commonRulesMenuItems = [
  26. {
  27. name: 'common-misspellings',
  28. description: 'editor_settings.common_settings.common_misspellings',
  29. },
  30. {
  31. name: 'max-comma',
  32. description: 'editor_settings.common_settings.max_comma',
  33. },
  34. {
  35. name: 'sentence-length',
  36. description: 'editor_settings.common_settings.sentence_length',
  37. },
  38. // { // omit because en-pos package is too big
  39. // name: 'en-capitalization',
  40. // description: 'editor_settings.common_settings.en_capitalization',
  41. // },
  42. {
  43. name: 'no-unmatched-pair',
  44. description: 'editor_settings.common_settings.no_unmatched_pair',
  45. },
  46. {
  47. name: 'date-weekday-mismatch',
  48. description: 'editor_settings.common_settings.date_weekday_mismatch',
  49. },
  50. {
  51. name: 'no-kangxi-radicals',
  52. description: 'editor_settings.common_settings.no_kangxi_radicals',
  53. },
  54. {
  55. name: 'no-surrogate-pair',
  56. description: 'editor_settings.common_settings.no_surrogate_pair',
  57. },
  58. {
  59. name: 'no-zero-width-spaces',
  60. description: 'editor_settings.common_settings.no_zero_width_spaces',
  61. },
  62. {
  63. name: 'period-in-list-item',
  64. description: 'editor_settings.common_settings.period_in_list_item',
  65. },
  66. {
  67. name: 'use-si-units',
  68. description: 'editor_settings.common_settings.use_si_units',
  69. },
  70. ];
  71. const japaneseRulesMenuItems = [
  72. {
  73. name: 'ja-hiragana-keishikimeishi',
  74. description: 'editor_settings.japanese_settings.ja_hiragana_keishikimeishi',
  75. },
  76. {
  77. name: 'ja-no-abusage',
  78. description: 'editor_settings.japanese_settings.ja_no_abusage',
  79. },
  80. {
  81. name: 'ja-no-inappropriate-words',
  82. description: 'editor_settings.japanese_settings.ja_no_inappropriate_words',
  83. },
  84. {
  85. name: 'ja-no-mixed-period',
  86. description: 'editor_settings.japanese_settings.ja_no_mixed_period',
  87. },
  88. {
  89. name: 'ja-no-redundant-expression',
  90. description: 'editor_settings.japanese_settings.ja_no_redundant_expression',
  91. },
  92. {
  93. name: 'max-kanji-continuous-len',
  94. description: 'editor_settings.japanese_settings.max_kanji_continuous_len',
  95. },
  96. {
  97. name: 'max-ten',
  98. description: 'editor_settings.japanese_settings.max_ten',
  99. },
  100. {
  101. name: 'no-double-negative-ja',
  102. description: 'editor_settings.japanese_settings.no_double_negative_ja',
  103. },
  104. {
  105. name: 'no-doubled-conjunction',
  106. description: 'editor_settings.japanese_settings.no_doubled_conjunction',
  107. },
  108. {
  109. name: 'no-doubled-joshi',
  110. description: 'editor_settings.japanese_settings.no_doubled_joshi',
  111. },
  112. {
  113. name: 'no-dropping-the-ra',
  114. description: 'editor_settings.japanese_settings.no_dropping_the_ra',
  115. },
  116. {
  117. name: 'no-hankaku-kana',
  118. description: 'editor_settings.japanese_settings.no_hankaku_kana',
  119. },
  120. {
  121. name: 'prefer-tari-tari',
  122. description: 'editor_settings.japanese_settings.prefer_tari_tari',
  123. },
  124. {
  125. name: 'ja-unnatural-alphabet',
  126. description: 'editor_settings.japanese_settings.ja_unnatural_alphabet',
  127. },
  128. {
  129. name: 'no-mixed-zenkaku-and-hankaku-alphabet',
  130. description: 'editor_settings.japanese_settings.no_mixed_zenkaku_and_hankaku_alphabet',
  131. },
  132. {
  133. name: 'no-nfd',
  134. description: 'editor_settings.japanese_settings.no_nfd',
  135. },
  136. ];
  137. const RuleListGroup: FC<RuleListGroupProps> = ({
  138. title, ruleList, textlintRules, setTextlintRules,
  139. }: RuleListGroupProps) => {
  140. const { t } = useTranslation();
  141. const isCheckedRule = (ruleName: string) => (
  142. textlintRules.find(stateRule => (
  143. stateRule.name === ruleName
  144. ))?.isEnabled || false
  145. );
  146. const ruleCheckboxHandler = (isChecked: boolean, ruleName: string) => {
  147. setTextlintRules(prevState => (
  148. prevState.filter(rule => rule.name !== ruleName).concat({ name: ruleName, isEnabled: isChecked })
  149. ));
  150. };
  151. return (
  152. <>
  153. <h2 className="border-bottom my-4">{t(title)}</h2>
  154. <div className="form-group row">
  155. <div className="offset-md-3 col-md-6 text-left">
  156. {ruleList.map(rule => (
  157. <div
  158. key={rule.name}
  159. className="custom-control custom-switch custom-checkbox-success"
  160. >
  161. <input
  162. type="checkbox"
  163. className="custom-control-input"
  164. id={rule.name}
  165. checked={isCheckedRule(rule.name)}
  166. onChange={e => ruleCheckboxHandler(e.target.checked, rule.name)}
  167. />
  168. <label className="custom-control-label" htmlFor={rule.name}>
  169. <strong>{rule.name}</strong>
  170. </label>
  171. <p className="form-text text-muted small">
  172. {t(rule.description)}
  173. </p>
  174. </div>
  175. ))}
  176. </div>
  177. </div>
  178. </>
  179. );
  180. };
  181. const createRulesFromDefaultList = (rule: { name: string }) => (
  182. {
  183. name: rule.name,
  184. isEnabled: true,
  185. }
  186. );
  187. export const EditorSettings = memo((): JSX.Element => {
  188. const { t } = useTranslation();
  189. const [textlintRules, setTextlintRules] = useState<LintRule[]>([]);
  190. const { data: dataEditorSettings, update: updateEditorSettings } = useEditorSettings();
  191. const defaultRules = useMemo(() => {
  192. const defaultCommonRules = commonRulesMenuItems.map(rule => createRulesFromDefaultList(rule));
  193. const defaultJapaneseRules = japaneseRulesMenuItems.map(rule => createRulesFromDefaultList(rule));
  194. return [...defaultCommonRules, ...defaultJapaneseRules];
  195. }, []);
  196. const initializeEditorSettings = useCallback(() => {
  197. if (dataEditorSettings == null) {
  198. return;
  199. }
  200. const retrievedRules: LintRule[] | undefined = dataEditorSettings?.textlintSettings?.textlintRules;
  201. // If database is empty, add default rules to state
  202. if (retrievedRules != null && retrievedRules.length > 0) {
  203. setTextlintRules(retrievedRules);
  204. return;
  205. }
  206. setTextlintRules(defaultRules);
  207. }, [dataEditorSettings, defaultRules]);
  208. const updateRulesHandler = useCallback(async() => {
  209. try {
  210. await updateEditorSettings({ textlintSettings: { textlintRules } });
  211. toastSuccess(t('toaster.update_successed', { target: 'Updated Textlint Settings', ns: 'commons' }));
  212. }
  213. catch (err) {
  214. toastError(err);
  215. }
  216. }, [t, textlintRules, updateEditorSettings]);
  217. useEffect(() => {
  218. initializeEditorSettings();
  219. }, [initializeEditorSettings]);
  220. if (textlintRules == null) {
  221. return (
  222. <div className="text-muted text-center">
  223. <i className="fa fa-2x fa-spinner fa-pulse"></i>
  224. </div>
  225. );
  226. }
  227. return (
  228. <div data-testid="grw-editor-settings">
  229. <RuleListGroup
  230. title="editor_settings.common_settings.common_settings"
  231. ruleList={commonRulesMenuItems}
  232. textlintRules={textlintRules}
  233. setTextlintRules={setTextlintRules}
  234. />
  235. <RuleListGroup
  236. title="editor_settings.japanese_settings.japanese_settings"
  237. ruleList={japaneseRulesMenuItems}
  238. textlintRules={textlintRules}
  239. setTextlintRules={setTextlintRules}
  240. />
  241. <div className="row my-3">
  242. <div className="offset-4 col-5">
  243. <button
  244. data-testid="grw-editor-settings-update-button"
  245. type="button"
  246. className="btn btn-primary"
  247. onClick={updateRulesHandler}
  248. >
  249. {t('Update')}
  250. </button>
  251. </div>
  252. </div>
  253. </div>
  254. );
  255. });
  256. EditorSettings.displayName = 'EditorSettings';