-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(issues): Generate AI issue view titles #107820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2571eea
abf4b4d
a1a12d0
d5b1fe6
1dbc13e
6b82509
467ede5
ec65fa0
1da5fba
f172cda
b6c5bbc
1f783c9
5473cbd
3eb5941
4aece5c
d651659
2fa29fc
41c7936
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import {act, renderHook} from 'sentry-test/reactTestingLibrary'; | ||
|
|
||
| import FormModel from 'sentry/components/forms/model'; | ||
|
|
||
| import {useFormTypingAnimation} from './useFormTypingAnimation'; | ||
|
|
||
| describe('useFormTypingAnimation', () => { | ||
| function useTestHook(props: {speed?: number}) { | ||
| return useFormTypingAnimation(props); | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| jest.useFakeTimers(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.useRealTimers(); | ||
| }); | ||
|
|
||
| it('animates text into the target form field', () => { | ||
| const formModel = new FormModel(); | ||
| const {result} = renderHook(useTestHook, { | ||
| initialProps: {speed: 80}, | ||
| }); | ||
|
|
||
| act(() => { | ||
| result.current.triggerFormTypingAnimation({ | ||
| formModel, | ||
| fieldName: 'name', | ||
| text: 'Hello', | ||
| }); | ||
| }); | ||
|
|
||
| expect(formModel.getValue('name')).toBe(''); | ||
|
|
||
| act(() => { | ||
| jest.advanceTimersByTime(48); | ||
| }); | ||
|
|
||
| const intermediateValue = formModel.getValue<string>('name') ?? ''; | ||
| expect(intermediateValue.length).toBeGreaterThan(0); | ||
| expect(intermediateValue.length).toBeLessThan('Hello'.length); | ||
|
|
||
| act(() => { | ||
| jest.runAllTimers(); | ||
| }); | ||
|
|
||
| expect(formModel.getValue('name')).toBe('Hello'); | ||
| }); | ||
|
|
||
| it('restarts animation when triggered again', () => { | ||
| const formModel = new FormModel(); | ||
| const {result} = renderHook(useTestHook, { | ||
| initialProps: {speed: 10}, | ||
| }); | ||
|
|
||
| act(() => { | ||
| result.current.triggerFormTypingAnimation({ | ||
| formModel, | ||
| fieldName: 'name', | ||
| text: 'First generated title', | ||
| }); | ||
| }); | ||
|
|
||
| act(() => { | ||
| jest.advanceTimersByTime(120); | ||
| }); | ||
|
|
||
| act(() => { | ||
| result.current.triggerFormTypingAnimation({ | ||
| formModel, | ||
| fieldName: 'name', | ||
| text: 'New title', | ||
| speed: 120, | ||
| }); | ||
| }); | ||
|
|
||
| act(() => { | ||
| jest.runAllTimers(); | ||
| }); | ||
|
|
||
| expect(formModel.getValue('name')).toBe('New title'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import {useCallback, useEffect, useRef} from 'react'; | ||
|
|
||
| import type FormModel from 'sentry/components/forms/model'; | ||
|
|
||
| interface TriggerFormTypingAnimationParams { | ||
| fieldName: string; | ||
| formModel: FormModel; | ||
| text: string; | ||
| quiet?: boolean; | ||
| speed?: number; | ||
| } | ||
|
|
||
| interface UseFormTypingAnimationOptions { | ||
| /** | ||
| * Typing speed in characters per second. | ||
| */ | ||
| speed?: number; | ||
| } | ||
|
|
||
| /** | ||
| * Animates text directly into a form field value. | ||
| */ | ||
| export function useFormTypingAnimation({ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice if we could use Framer to do this, but I can see how it would be difficult with the form. Maybe another thing to look at when we try to update the form components |
||
| speed: defaultSpeed = 70, | ||
| }: UseFormTypingAnimationOptions = {}) { | ||
| const animationFrameRef = useRef<number | null>(null); | ||
| const currentIndexRef = useRef(0); | ||
| const lastUpdateTimeRef = useRef(0); | ||
| const runIdRef = useRef(0); | ||
|
|
||
| const cancelFormTypingAnimation = useCallback(() => { | ||
| runIdRef.current += 1; | ||
| if (animationFrameRef.current !== null) { | ||
| window.cancelAnimationFrame(animationFrameRef.current); | ||
| animationFrameRef.current = null; | ||
| } | ||
| }, []); | ||
|
|
||
| useEffect(() => cancelFormTypingAnimation, [cancelFormTypingAnimation]); | ||
|
|
||
| const triggerFormTypingAnimation = useCallback( | ||
| ({ | ||
| formModel, | ||
| fieldName, | ||
| text, | ||
| speed = defaultSpeed, | ||
| }: TriggerFormTypingAnimationParams) => { | ||
| cancelFormTypingAnimation(); | ||
|
|
||
| const runId = runIdRef.current; | ||
|
|
||
| if (!text.length) { | ||
| formModel.setValue(fieldName, '', {quiet: true}); | ||
| return; | ||
| } | ||
|
|
||
| currentIndexRef.current = 0; | ||
| lastUpdateTimeRef.current = performance.now(); | ||
| formModel.setValue(fieldName, '', {quiet: true}); | ||
|
|
||
| const interval = 1000 / Math.max(1, speed); | ||
|
|
||
| const animate = (timestamp: number) => { | ||
| if (runIdRef.current !== runId) { | ||
| return; | ||
| } | ||
|
|
||
| const elapsed = timestamp - lastUpdateTimeRef.current; | ||
| const charsToAdd = Math.floor(elapsed / interval); | ||
|
|
||
| if (charsToAdd > 0) { | ||
| const nextIndex = Math.min(text.length, currentIndexRef.current + charsToAdd); | ||
| if (nextIndex > currentIndexRef.current) { | ||
| formModel.setValue(fieldName, text.slice(0, nextIndex), {quiet: true}); | ||
| currentIndexRef.current = nextIndex; | ||
| lastUpdateTimeRef.current = timestamp; | ||
| } | ||
| } | ||
|
|
||
| if (currentIndexRef.current < text.length) { | ||
| animationFrameRef.current = window.requestAnimationFrame(animate); | ||
| return; | ||
| } | ||
|
|
||
| animationFrameRef.current = null; | ||
| // The last setValue is not quiet to trigger form validation | ||
| formModel.setValue(fieldName, text); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Final setValue ignores quiet parameter after animationMedium Severity The final |
||
| }; | ||
|
|
||
| animationFrameRef.current = window.requestAnimationFrame(animate); | ||
| }, | ||
| [cancelFormTypingAnimation, defaultSpeed] | ||
| ); | ||
|
|
||
| return {triggerFormTypingAnimation, cancelFormTypingAnimation}; | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the right place for this hook to live? Seems like something that should be in a common directory