|
1 | 1 | import Link from '@docusaurus/Link';
|
| 2 | +import { useHistory } from '@docusaurus/router'; |
2 | 3 | import type { RulesMeta } from '@site/rulesMeta';
|
3 | 4 | import { useRulesMeta } from '@site/src/hooks/useRulesMeta';
|
4 | 5 | import clsx from 'clsx';
|
5 |
| -import React, { useMemo, useState } from 'react'; |
| 6 | +import React, { useMemo } from 'react'; |
6 | 7 |
|
| 8 | +import { |
| 9 | + type HistorySelector, |
| 10 | + useHistorySelector, |
| 11 | +} from '../../hooks/useHistorySelector'; |
7 | 12 | import styles from './styles.module.css';
|
8 | 13 |
|
9 | 14 | function interpolateCode(text: string): (JSX.Element | string)[] | string {
|
@@ -118,82 +123,60 @@ function match(mode: FilterMode, value: boolean): boolean | undefined {
|
118 | 123 | }
|
119 | 124 |
|
120 | 125 | export default function RulesTable({
|
121 |
| - extensionRules, |
| 126 | + ruleset, |
122 | 127 | }: {
|
123 |
| - extensionRules?: boolean; |
| 128 | + ruleset: 'extension-rules' | 'supported-rules'; |
124 | 129 | }): JSX.Element {
|
| 130 | + const [filters, changeFilter] = useRulesFilters(ruleset); |
| 131 | + |
125 | 132 | const rules = useRulesMeta();
|
126 |
| - const [showRecommended, setShowRecommended] = useState<FilterMode>('neutral'); |
127 |
| - const [showStrict, setShowStrict] = useState<FilterMode>('neutral'); |
128 |
| - const [showFixable, setShowFixable] = useState<FilterMode>('neutral'); |
129 |
| - const [showHasSuggestions, setShowHasSuggestion] = |
130 |
| - useState<FilterMode>('neutral'); |
131 |
| - const [showTypeCheck, setShowTypeCheck] = useState<FilterMode>('neutral'); |
| 133 | + const extensionRules = ruleset === 'extension-rules'; |
132 | 134 | const relevantRules = useMemo(
|
133 | 135 | () =>
|
134 | 136 | rules
|
135 | 137 | .filter(r => !!extensionRules === !!r.docs?.extendsBaseRule)
|
136 | 138 | .filter(r => {
|
137 | 139 | const opinions = [
|
138 | 140 | match(
|
139 |
| - showRecommended, |
| 141 | + filters.recommended, |
140 | 142 | r.docs?.recommended === 'error' || r.docs?.recommended === 'warn',
|
141 | 143 | ),
|
142 |
| - match(showStrict, r.docs?.recommended === 'strict'), |
143 |
| - match(showFixable, !!r.fixable), |
144 |
| - match(showHasSuggestions, !!r.hasSuggestions), |
145 |
| - match(showTypeCheck, !!r.docs?.requiresTypeChecking), |
| 144 | + match(filters.strict, r.docs?.recommended === 'strict'), |
| 145 | + match(filters.fixable, !!r.fixable), |
| 146 | + match(filters.suggestions, !!r.hasSuggestions), |
| 147 | + match(filters.typeInformation, !!r.docs?.requiresTypeChecking), |
146 | 148 | ].filter((o): o is boolean => o !== undefined);
|
147 | 149 | return opinions.every(o => o);
|
148 | 150 | }),
|
149 |
| - [ |
150 |
| - rules, |
151 |
| - extensionRules, |
152 |
| - showRecommended, |
153 |
| - showStrict, |
154 |
| - showFixable, |
155 |
| - showHasSuggestions, |
156 |
| - showTypeCheck, |
157 |
| - ], |
| 151 | + [rules, extensionRules, filters], |
158 | 152 | );
|
| 153 | + |
159 | 154 | return (
|
160 | 155 | <>
|
161 | 156 | <ul className={clsx('clean-list', styles.checkboxList)}>
|
162 | 157 | <RuleFilterCheckBox
|
163 |
| - mode={showRecommended} |
164 |
| - setMode={(newMode): void => { |
165 |
| - setShowRecommended(newMode); |
166 |
| - |
167 |
| - if (newMode === 'include' && showStrict === 'include') { |
168 |
| - setShowStrict('exclude'); |
169 |
| - } |
170 |
| - }} |
| 158 | + mode={filters.recommended} |
| 159 | + setMode={(newMode): void => changeFilter('recommended', newMode)} |
171 | 160 | label="✅ recommended"
|
172 | 161 | />
|
173 | 162 | <RuleFilterCheckBox
|
174 |
| - mode={showStrict} |
175 |
| - setMode={(newMode): void => { |
176 |
| - setShowStrict(newMode); |
177 |
| - |
178 |
| - if (newMode === 'include' && showRecommended === 'include') { |
179 |
| - setShowRecommended('exclude'); |
180 |
| - } |
181 |
| - }} |
| 163 | + mode={filters.strict} |
| 164 | + setMode={(newMode): void => changeFilter('strict', newMode)} |
182 | 165 | label="🔒 strict"
|
183 | 166 | />
|
184 | 167 | <RuleFilterCheckBox
|
185 |
| - mode={showFixable} |
186 |
| - setMode={setShowFixable} |
| 168 | + mode={filters.fixable} |
| 169 | + setMode={(newMode): void => changeFilter('fixable', newMode)} |
187 | 170 | label="🔧 fixable"
|
188 | 171 | />
|
189 | 172 | <RuleFilterCheckBox
|
190 |
| - mode={showHasSuggestions} |
191 |
| - setMode={setShowHasSuggestion} |
| 173 | + mode={filters.suggestions} |
| 174 | + setMode={(newMode): void => changeFilter('suggestions', newMode)} |
192 | 175 | label="💡 has suggestions"
|
193 | 176 | />
|
194 | 177 | <RuleFilterCheckBox
|
195 |
| - mode={showTypeCheck} |
196 |
| - setMode={setShowTypeCheck} |
| 178 | + mode={filters.typeInformation} |
| 179 | + setMode={(newMode): void => changeFilter('typeInformation', newMode)} |
197 | 180 | label="💭 requires type information"
|
198 | 181 | />
|
199 | 182 | </ul>
|
@@ -224,3 +207,97 @@ export default function RulesTable({
|
224 | 207 | </>
|
225 | 208 | );
|
226 | 209 | }
|
| 210 | + |
| 211 | +type FilterCategory = |
| 212 | + | 'recommended' |
| 213 | + | 'strict' |
| 214 | + | 'fixable' |
| 215 | + | 'suggestions' |
| 216 | + | 'typeInformation'; |
| 217 | +type FiltersState = Record<FilterCategory, FilterMode>; |
| 218 | +const neutralFiltersState: FiltersState = { |
| 219 | + recommended: 'neutral', |
| 220 | + strict: 'neutral', |
| 221 | + fixable: 'neutral', |
| 222 | + suggestions: 'neutral', |
| 223 | + typeInformation: 'neutral', |
| 224 | +}; |
| 225 | + |
| 226 | +const selectSearch: HistorySelector<string> = history => |
| 227 | + history.location.search; |
| 228 | +const getServerSnapshot = (): string => ''; |
| 229 | + |
| 230 | +function useRulesFilters( |
| 231 | + paramsKey: string, |
| 232 | +): [FiltersState, (category: FilterCategory, mode: FilterMode) => void] { |
| 233 | + const history = useHistory(); |
| 234 | + const search = useHistorySelector(selectSearch, getServerSnapshot); |
| 235 | + |
| 236 | + const paramValue = new URLSearchParams(search).get(paramsKey) ?? ''; |
| 237 | + // We can't compute this in selectSearch, because we need the snapshot to be |
| 238 | + // comparable by value. |
| 239 | + const filtersState = useMemo( |
| 240 | + () => parseFiltersState(paramValue), |
| 241 | + [paramValue], |
| 242 | + ); |
| 243 | + |
| 244 | + const changeFilter = (category: FilterCategory, mode: FilterMode): void => { |
| 245 | + const newState = { ...filtersState, [category]: mode }; |
| 246 | + |
| 247 | + if ( |
| 248 | + category === 'strict' && |
| 249 | + mode === 'include' && |
| 250 | + filtersState.recommended === 'include' |
| 251 | + ) { |
| 252 | + newState.recommended = 'exclude'; |
| 253 | + } else if ( |
| 254 | + category === 'recommended' && |
| 255 | + mode === 'include' && |
| 256 | + filtersState.strict === 'include' |
| 257 | + ) { |
| 258 | + newState.strict = 'exclude'; |
| 259 | + } |
| 260 | + |
| 261 | + const searchParams = new URLSearchParams(history.location.search); |
| 262 | + const filtersString = stringifyFiltersState(newState); |
| 263 | + |
| 264 | + if (filtersString) { |
| 265 | + searchParams.set(paramsKey, filtersString); |
| 266 | + } else { |
| 267 | + searchParams.delete(paramsKey); |
| 268 | + } |
| 269 | + |
| 270 | + history.replace({ search: searchParams.toString() }); |
| 271 | + }; |
| 272 | + |
| 273 | + return [filtersState, changeFilter]; |
| 274 | +} |
| 275 | + |
| 276 | +const NEGATION_SYMBOL = 'x'; |
| 277 | + |
| 278 | +function stringifyFiltersState(filters: FiltersState): string { |
| 279 | + return Object.entries(filters) |
| 280 | + .map(([key, value]) => |
| 281 | + value === 'include' |
| 282 | + ? key |
| 283 | + : value === 'exclude' |
| 284 | + ? `${NEGATION_SYMBOL}${key}` |
| 285 | + : '', |
| 286 | + ) |
| 287 | + .filter(Boolean) |
| 288 | + .join('-'); |
| 289 | +} |
| 290 | + |
| 291 | +function parseFiltersState(str: string): FiltersState { |
| 292 | + const res: FiltersState = { ...neutralFiltersState }; |
| 293 | + |
| 294 | + for (const part of str.split('-')) { |
| 295 | + const exclude = part.startsWith(NEGATION_SYMBOL); |
| 296 | + const key = exclude ? part.slice(1) : part; |
| 297 | + if (Object.hasOwn(neutralFiltersState, key)) { |
| 298 | + res[key] = exclude ? 'exclude' : 'include'; |
| 299 | + } |
| 300 | + } |
| 301 | + |
| 302 | + return res; |
| 303 | +} |
0 commit comments