From 1837781b3053715e8fecf272fef9c5f1394eef7e Mon Sep 17 00:00:00 2001 From: Steven Chung Date: Wed, 22 Nov 2023 15:30:42 -0500 Subject: [PATCH 1/4] use fixtures in instagram TestPost_ExtractToDir --- pkg/extractor/instagram/instagram_test.go | 15 ++++----------- .../testdata/TestPost_ExtractToDir/fake.json | 12 ++++++++++++ 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 pkg/extractor/instagram/testdata/TestPost_ExtractToDir/fake.json diff --git a/pkg/extractor/instagram/instagram_test.go b/pkg/extractor/instagram/instagram_test.go index 8792c43e..6561b6df 100644 --- a/pkg/extractor/instagram/instagram_test.go +++ b/pkg/extractor/instagram/instagram_test.go @@ -65,21 +65,19 @@ func TestSource_ID(t *testing.T) { } } -var extractToDirSuffixes = []int{0, 1, 2, 9, 10, 11, 99, 100, 101, 110} - -const extractToDirPrefix = "2023-11-21_10-42-44_UTC_" - func init() { + extractToDirSuffixes := []int{0, 1, 2, 9, 10, 11, 99, 100, 101, 110} args := make([]string, len(extractToDirSuffixes)+1) args[0] = "touch" for i, suffix := range extractToDirSuffixes { - args[i+1] = extractToDirPrefix + strconv.Itoa(suffix) + extensions[0] + args[i+1] = "2023-11-21_10-42-44_UTC_" + strconv.Itoa(suffix) + extensions[0] } extractToDirArgs = func(login, id string) []string { return args } } func TestPost_ExtractToDir(t *testing.T) { + testName := "TestPost_ExtractToDir" cacheDir := path.Join(os.TempDir(), test.GenerateName("Instagram")) require.NoError(t, os.MkdirAll(cacheDir, ioutil.OwnerRWXGroupRX)) @@ -108,12 +106,7 @@ func TestPost_ExtractToDir(t *testing.T) { for i, entry := range entries { entryNames[i] = entry.Name() } - - filenames := make([]string, len(extractToDirSuffixes)) - for i, suffix := range extractToDirSuffixes { - filenames[i] = extractToDirPrefix + fmt.Sprintf("%03d", suffix) + extensions[0] - } - require.Equal(filenames, entryNames) + fixture.CompareReadOrUpdate(t, filepath.Join(testName, tc.name+".json"), fixture.JSON(t, entryNames)) }) } } diff --git a/pkg/extractor/instagram/testdata/TestPost_ExtractToDir/fake.json b/pkg/extractor/instagram/testdata/TestPost_ExtractToDir/fake.json new file mode 100644 index 00000000..e7e974ed --- /dev/null +++ b/pkg/extractor/instagram/testdata/TestPost_ExtractToDir/fake.json @@ -0,0 +1,12 @@ +[ + "2023-11-21_10-42-44_UTC_000.jpg", + "2023-11-21_10-42-44_UTC_001.jpg", + "2023-11-21_10-42-44_UTC_002.jpg", + "2023-11-21_10-42-44_UTC_009.jpg", + "2023-11-21_10-42-44_UTC_010.jpg", + "2023-11-21_10-42-44_UTC_011.jpg", + "2023-11-21_10-42-44_UTC_099.jpg", + "2023-11-21_10-42-44_UTC_100.jpg", + "2023-11-21_10-42-44_UTC_101.jpg", + "2023-11-21_10-42-44_UTC_110.jpg" +] \ No newline at end of file From 62ea016e196aa52e1b68eb68648ef03740aff597 Mon Sep 17 00:00:00 2001 From: Steven Chung Date: Wed, 22 Nov 2023 16:38:56 -0500 Subject: [PATCH 2/4] change createNoteData flow to SourceNavComponent --- ui/src/components/notes/NoteCreate.tsx | 13 +- ui/src/components/sources/SourceShow.tsx | 147 ++++++++++-------- .../sources/SourceShow_SourceNavComponent.ts | 35 ++--- .../sources/SourceShow_TermsComponent.ts | 12 +- .../sources/SourceShow_TokensComponent.ts | 6 +- 5 files changed, 110 insertions(+), 103 deletions(-) diff --git a/ui/src/components/notes/NoteCreate.tsx b/ui/src/components/notes/NoteCreate.tsx index f5fa8f39..c0b303b0 100644 --- a/ui/src/components/notes/NoteCreate.tsx +++ b/ui/src/components/notes/NoteCreate.tsx @@ -20,10 +20,11 @@ const otherKeys = filterKeys( [commonLevelKey], ) -const NoteCreate: React.FC<{ readonly data: CreateNoteData; readonly onClose: () => void }> = ({ - data, - onClose, -}) => { +const NoteCreate: React.FC<{ + readonly data: CreateNoteData + readonly onCreate: () => void + readonly onClose: () => void +}> = ({ data, onCreate, onClose }) => { const fetcher = useFetcher() const { error, success } = useContext(NotificationsContext) useEffect(() => { @@ -31,8 +32,8 @@ const NoteCreate: React.FC<{ readonly data: CreateNoteData; readonly onClose: () fetcher.data.note .then((note) => success(`Created new Note: ${note.text}`)) .catch(() => error("Failed to create Note")) - .finally(() => onClose()) - }, [fetcher, onClose, success, error]) + .finally(() => onCreate()) + }, [fetcher, onClose, success, error, onCreate]) const [submitted, setSubmitted] = useState(false) const submitButtonRef = useRef(null) diff --git a/ui/src/components/sources/SourceShow.tsx b/ui/src/components/sources/SourceShow.tsx index a2e95ecf..70ea8fcb 100644 --- a/ui/src/components/sources/SourceShow.tsx +++ b/ui/src/components/sources/SourceShow.tsx @@ -18,11 +18,7 @@ import AwaitWithFallback from "../AwaitWithFallback.tsx" import SlideOver from "../SlideOver.tsx" import NoteCreate from "../notes/NoteCreate.tsx" import { StopKeyboardContext, useStopKeyboard } from "./SourceShow_SourceComponent.ts" -import { - getTermProps, - ITermsComponentProps, - useFocusTextWithKeyboard, -} from "./SourceShow_SourceNavComponent.ts" +import { getUsage, useFocusTextWithKeyboard } from "./SourceShow_SourceNavComponent.ts" import { otherTranslationTexts, useChangeTermWithKeyboard } from "./SourceShow_TermsComponent.ts" import { useFocusTokenWithKeyboard } from "./SourceShow_TokensComponent.ts" import { @@ -32,7 +28,7 @@ import { SourceEditHeader, SourcePartDetailMenu, } from "./SourceShow_Update.tsx" -import React, { useEffect, useMemo, useRef, useState } from "react" +import React, { useContext, useEffect, useMemo, useRef, useState } from "react" import { useFetcher } from "react-router-dom" export interface ISourceShowData { @@ -237,22 +233,29 @@ const SourceShowComponent: React.FC<{ readonly source: Source }> = ({ source }) ) } +// eslint-disable-next-line max-lines-per-function const SourceNavComponent: React.FC<{ readonly source: Source; readonly safeSet: SafeSet }> = ({ source, safeSet, }) => { - const [termProps, setTermProps] = useState(null) + const [createNoteData, setCreateNoteData] = useState(null) + const { setStopKeyboardEvents } = useContext(StopKeyboardContext) + useEffect( + () => setStopKeyboardEvents(createNoteData !== null), + [createNoteData, setStopKeyboardEvents], + ) - const termsFocused = termProps !== null + const [selectedToken, setSelectedToken] = useState(null) + const isTokenSelected = selectedToken !== null const [partFocusIndex, textFocusIndex, focusElement, setText] = useFocusTextWithKeyboard( source.parts, - termsFocused, - () => setTermProps(null), + isTokenSelected, + () => setSelectedToken(null), ) const textRefs = useRef<(HTMLDivElement | null)[][]>([]) useEffect(() => { - setTermProps(null) + setSelectedToken(null) const textElement = textRefs.current[partFocusIndex][textFocusIndex] if (!textElement) return focusElement(textElement) @@ -260,52 +263,74 @@ const SourceNavComponent: React.FC<{ readonly source: Source; readonly safeSet: }, [focusElement, partFocusIndex, textFocusIndex]) return ( - - {(tokenizedText, partIndex, textIndex) => { - const textFocused = partIndex === partFocusIndex && textIndex === textFocusIndex - return ( -
{ - if (!textRefs.current[partIndex]) textRefs.current[partIndex] = [] - textRefs.current[partIndex][textIndex] = ref - }} - tabIndex={-1} - className={joinClasses(textFocused ? "py-4 bg-gray-std" : "", "group py-2")} - onClick={preventDefault(() => setText(partIndex, textIndex))} - > -
- {tokenizedText.text} -
- {textFocused ? ( - - setTermProps(getTermProps(source, partFocusIndex, textFocusIndex, tokenIndex)) - } - onTokenChange={(tokenElement) => focusElement(tokenElement)} - /> - ) : null} -
- {tokenizedText.translation} + <> + {createNoteData !== null && ( + { + setCreateNoteData(null) + setSelectedToken(null) + }} + onClose={() => { + setCreateNoteData(null) + }} + /> + )} + + {(tokenizedText, partIndex, textIndex) => { + const textFocused = partIndex === partFocusIndex && textIndex === textFocusIndex + return ( +
{ + if (!textRefs.current[partIndex]) textRefs.current[partIndex] = [] + textRefs.current[partIndex][textIndex] = ref + }} + tabIndex={-1} + className={joinClasses(textFocused ? "py-4 bg-gray-std" : "", "group py-2")} + onClick={preventDefault(() => setText(partIndex, textIndex))} + > +
+ {tokenizedText.text} +
+ {textFocused ? ( + setSelectedToken(tokenizedText.tokens[tokenIndex])} + onTokenChange={(tokenElement) => focusElement(tokenElement)} + /> + ) : null} +
+ {tokenizedText.translation} +
+ {textFocused && selectedToken ? ( + { + setCreateNoteData( + createNoteDataFromSourceTerm( + term, + getUsage(source, partFocusIndex, textFocusIndex), + ), + ) + }} + /> + ) : null}
- {textFocused && termsFocused ? ( - - ) : null} -
- ) - }} - + ) + }} + + ) } const TokensComponent: React.FC<{ readonly tokens: Token[] - readonly termsFocused: boolean + readonly isTokenSelected: boolean readonly onTokenSelect: (tokenFocusIndex: number) => void readonly onTokenChange: (tokenElement: HTMLDivElement) => void -}> = ({ tokens, termsFocused, onTokenSelect, onTokenChange }) => { - const [tokenFocusIndex] = useFocusTokenWithKeyboard(tokens, termsFocused, onTokenSelect) +}> = ({ tokens, isTokenSelected, onTokenSelect, onTokenChange }) => { + const [tokenFocusIndex] = useFocusTokenWithKeyboard(tokens, isTokenSelected, onTokenSelect) const tokenRefs = useRef<(HTMLDivElement | null)[]>([]) useEffect(() => { @@ -351,7 +376,10 @@ interface ITermsShowData { const termsComponentClass = "grid-std text-left text-lg py-2 space-y-2" -const TermsComponent: React.FC = ({ token, usage }) => { +const TermsComponent: React.FC<{ + readonly token: Token + readonly onTermSelect: (term: Term) => void +}> = ({ token, onTermSelect }) => { const fetcher = useFetcher() const terms = useMemo(() => (fetcher.data ? fetcher.data.terms : []), [fetcher.data]) useEffect(() => { @@ -359,11 +387,9 @@ const TermsComponent: React.FC = ({ token, usage }) => { fetcher.load(`/terms/search?${queryString({ query: token.text, pos: token.partOfSpeech })}`) }, [fetcher, token]) - const [createNoteData, setCreateNoteData] = useState(null) const [termFocusIndex, pageIndex, pagesLen, maxPageSize, shake] = useChangeTermWithKeyboard( terms, - (term: Term) => setCreateNoteData(createNoteDataFromSourceTerm(term, usage)), - () => createNoteData !== null, + onTermSelect, ) const termRefs = useRef<(HTMLDivElement | null)[]>([]) @@ -407,22 +433,19 @@ const TermsComponent: React.FC = ({ token, usage }) => {
)} - - {createNoteData !== null && ( - setCreateNoteData(null)} /> - )} ) } -const NoteDialog: React.FC<{ readonly data: CreateNoteData; readonly onClose: () => void }> = ({ - data, - onClose, -}) => { +const CreateNoteDialog: React.FC<{ + readonly data: CreateNoteData + readonly onCreate: () => void + readonly onClose: () => void +}> = ({ data, onCreate, onClose }) => { return ( - + ) } diff --git a/ui/src/components/sources/SourceShow_SourceNavComponent.ts b/ui/src/components/sources/SourceShow_SourceNavComponent.ts index 21f0cb63..50f6a83c 100644 --- a/ui/src/components/sources/SourceShow_SourceNavComponent.ts +++ b/ui/src/components/sources/SourceShow_SourceNavComponent.ts @@ -1,5 +1,4 @@ -import { NoteUsage } from "../../services/models/Note.ts" -import { Source, SourcePart, Token } from "../../services/models/Source.ts" +import { Source, SourcePart } from "../../services/models/Source.ts" import { useKeyDownEffect } from "../../utils/JSXUtil.ts" import { decrement, increment } from "../../utils/NumberUtil.ts" import { StopKeyboardContext } from "./SourceShow_SourceComponent.ts" @@ -7,7 +6,7 @@ import { useCallback, useContext, useState } from "react" export function useFocusTextWithKeyboard( parts: SourcePart[], - entered: boolean, + isTokenSelected: boolean, onEscape: () => void, ): readonly [ number, @@ -26,14 +25,14 @@ export function useFocusTextWithKeyboard( switch (e.code) { case "Escape": - if (!entered) return + if (!isTokenSelected) return focusLastElement() onEscape() break default: } - if (entered) return + if (isTokenSelected) return switch (e.code) { case "ArrowUp": @@ -51,33 +50,19 @@ export function useFocusTextWithKeyboard( e.preventDefault() }, - [stopKeyboardEvents, entered, focusLastElement, onEscape, decrementText, incrementText], + [stopKeyboardEvents, isTokenSelected, focusLastElement, onEscape, decrementText, incrementText], ) return [partFocusIndex, textFocusIndex, focusElement, setText] as const } -export interface ITermsComponentProps { - token: Token - usage: NoteUsage -} - -// eslint-disable-next-line max-params -export function getTermProps( - source: Source, - partFocusIndex: number, - textFocusIndex: number, - tokenFocusIndex: number, -): ITermsComponentProps { +export function getUsage(source: Source, partFocusIndex: number, textFocusIndex: number) { const tokenizedText = source.parts[partFocusIndex].tokenizedTexts[textFocusIndex] return { - token: tokenizedText.tokens[tokenFocusIndex], - usage: { - sourceName: source.name, - sourceReference: source.reference, + sourceName: source.name, + sourceReference: source.reference, - usage: tokenizedText.text, - usageTranslation: tokenizedText.translation, - }, + usage: tokenizedText.text, + usageTranslation: tokenizedText.translation, } } diff --git a/ui/src/components/sources/SourceShow_TermsComponent.ts b/ui/src/components/sources/SourceShow_TermsComponent.ts index 139fe5b3..45fcc23a 100644 --- a/ui/src/components/sources/SourceShow_TermsComponent.ts +++ b/ui/src/components/sources/SourceShow_TermsComponent.ts @@ -4,23 +4,21 @@ import { pageSize, totalPages } from "../../utils/HtmlUtil.ts" import { useKeyDownEffect, useTimedState } from "../../utils/JSXUtil.ts" import { decrement, increment } from "../../utils/NumberUtil.ts" import { StopKeyboardContext } from "./SourceShow_SourceComponent.ts" -import { useContext, useEffect, useMemo, useState } from "react" +import { useContext, useMemo, useState } from "react" const maxPageSize = 5 export function useChangeTermWithKeyboard( terms: Term[], - onEnter: (term: Term) => void, - isEntered: () => boolean, + onTermSelect: (term: Term) => void, ): readonly [number, number, number, number, boolean] { const [termFocusIndex, setTermFocusIndex] = useState(0) const [pageIndex, setPageIndex] = useState(0) const pagesLen = useMemo(() => totalPages(terms, maxPageSize), [terms]) const [shake, setShake] = useTimedState(100) - const { stopKeyboardEvents, setStopKeyboardEvents } = useContext(StopKeyboardContext) + const { stopKeyboardEvents } = useContext(StopKeyboardContext) - useEffect(() => setStopKeyboardEvents(isEntered()), [isEntered, setStopKeyboardEvents]) useKeyDownEffect( (e: KeyboardEvent) => { if (stopKeyboardEvents) return @@ -55,14 +53,14 @@ export function useChangeTermWithKeyboard( break case "Enter": case "Space": - onEnter(terms[termFocusIndex]) + onTermSelect(terms[termFocusIndex]) break default: return } e.preventDefault() }, - [stopKeyboardEvents, termFocusIndex, terms, pageIndex, pagesLen, onEnter, setShake], + [stopKeyboardEvents, termFocusIndex, terms, pageIndex, pagesLen, onTermSelect, setShake], ) return [termFocusIndex, pageIndex, pagesLen, maxPageSize, shake] as const } diff --git a/ui/src/components/sources/SourceShow_TokensComponent.ts b/ui/src/components/sources/SourceShow_TokensComponent.ts index 09decd9c..d3ac90a8 100644 --- a/ui/src/components/sources/SourceShow_TokensComponent.ts +++ b/ui/src/components/sources/SourceShow_TokensComponent.ts @@ -6,7 +6,7 @@ import { useContext, useMemo, useState } from "react" export function useFocusTokenWithKeyboard( tokens: Token[], - termsFocused: boolean, + isTokenSelected: boolean, onTokenSelect: (tokenFocusIndex: number) => void, ): readonly [number] { const [tokenFocusIndex, setTokenFocusIndex] = useState(0) @@ -18,7 +18,7 @@ export function useFocusTokenWithKeyboard( const { stopKeyboardEvents } = useContext(StopKeyboardContext) useKeyDownEffect( (e: KeyboardEvent) => { - if (stopKeyboardEvents || termsFocused || isAllPunct) return + if (stopKeyboardEvents || isTokenSelected || isAllPunct) return switch (e.code) { case "ArrowLeft": @@ -39,7 +39,7 @@ export function useFocusTokenWithKeyboard( e.preventDefault() }, - [stopKeyboardEvents, termsFocused, isAllPunct, tokens, tokenFocusIndex, onTokenSelect], + [stopKeyboardEvents, isTokenSelected, isAllPunct, tokens, tokenFocusIndex, onTokenSelect], ) return [tokenFocusIndex] as const } From bbb2ba06500316d5980f03d572f62d4c97dacc07 Mon Sep 17 00:00:00 2001 From: Steven Chung Date: Wed, 22 Nov 2023 17:26:59 -0500 Subject: [PATCH 3/4] focus textArea when updating part --- ui/src/components/sources/SourceShow_Update.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/components/sources/SourceShow_Update.tsx b/ui/src/components/sources/SourceShow_Update.tsx index 4aba7d39..fd31f4cc 100644 --- a/ui/src/components/sources/SourceShow_Update.tsx +++ b/ui/src/components/sources/SourceShow_Update.tsx @@ -203,13 +203,16 @@ export const PartUpdateForm: React.FC<{ onCancel() }, [fetcher, success, onCancel]) + const textAreaRef = useRef(null) + useEffect(() => textAreaRef.current?.focus(), []) + return ( -