Skip to content

Commit d29fe1e

Browse files
committed
Create a slight delay in on shortcut keys so you can't spam them and open multiple tabs.
1 parent 06d8de3 commit d29fe1e

File tree

3 files changed

+190
-2
lines changed

3 files changed

+190
-2
lines changed

.changeset/ninety-shirts-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme': patch
3+
---
4+
5+
Create a slight delay when keypressing theme dev shortcut keys to stop accidental copy pasting and opening up a ton of tabs

packages/theme/src/cli/services/dev.test.ts

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {openURLSafely, renderLinks} from './dev.js'
2-
import {describe, expect, test, vi} from 'vitest'
1+
import {openURLSafely, renderLinks, createKeypressHandler} from './dev.js'
2+
import {describe, expect, test, vi, beforeEach, afterEach} from 'vitest'
33
import {buildTheme} from '@shopify/cli-kit/node/themes/factories'
44
import {DEVELOPMENT_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils'
55
import {renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui'
@@ -102,3 +102,134 @@ describe('openURLSafely', () => {
102102
})
103103
})
104104
})
105+
106+
describe('createKeypressHandler', () => {
107+
const urls = {
108+
local: 'http://127.0.0.1:9292',
109+
giftCard: 'http://127.0.0.1:9292/gift_cards/[store_id]/preview',
110+
themeEditor: 'https://my-store.myshopify.com/admin/themes/123/editor?hr=9292',
111+
preview: 'https://my-store.myshopify.com/?preview_theme_id=123',
112+
}
113+
114+
const ctx = {lastRequestedPath: '/'}
115+
116+
beforeEach(() => {
117+
vi.mocked(openURL).mockResolvedValue(true)
118+
vi.useFakeTimers()
119+
})
120+
121+
afterEach(() => {
122+
vi.useRealTimers()
123+
})
124+
125+
test('opens localhost when "t" is pressed', () => {
126+
// Given
127+
const handler = createKeypressHandler(urls, ctx)
128+
129+
// When
130+
handler('t', {name: 't'})
131+
132+
// Then
133+
expect(openURL).toHaveBeenCalledWith(urls.local)
134+
})
135+
136+
test('opens theme preview when "p" is pressed', () => {
137+
// Given
138+
const handler = createKeypressHandler(urls, ctx)
139+
140+
// When
141+
handler('p', {name: 'p'})
142+
143+
// Then
144+
expect(openURL).toHaveBeenCalledWith(urls.preview)
145+
})
146+
147+
test('opens theme editor when "e" is pressed', () => {
148+
// Given
149+
const handler = createKeypressHandler(urls, ctx)
150+
151+
// When
152+
handler('e', {name: 'e'})
153+
154+
// Then
155+
expect(openURL).toHaveBeenCalledWith(urls.themeEditor)
156+
})
157+
158+
test('opens gift card preview when "g" is pressed', () => {
159+
// Given
160+
const handler = createKeypressHandler(urls, ctx)
161+
162+
// When
163+
handler('g', {name: 'g'})
164+
165+
// Then
166+
expect(openURL).toHaveBeenCalledWith(urls.giftCard)
167+
})
168+
169+
test('appends preview path to theme editor URL when lastRequestedPath is not "/"', () => {
170+
// Given
171+
const ctxWithPath = {lastRequestedPath: '/products/test-product'}
172+
const handler = createKeypressHandler(urls, ctxWithPath)
173+
174+
// When
175+
handler('e', {name: 'e'})
176+
177+
// Then
178+
expect(openURL).toHaveBeenCalledWith(
179+
`${urls.themeEditor}&previewPath=${encodeURIComponent('/products/test-product')}`,
180+
)
181+
})
182+
183+
test('debounces rapid keypresses - only opens URL once during debounce window', () => {
184+
// Given
185+
const handler = createKeypressHandler(urls, ctx)
186+
187+
// When
188+
handler('t', {name: 't'})
189+
handler('t', {name: 't'})
190+
handler('t', {name: 't'})
191+
handler('t', {name: 't'})
192+
193+
// Then
194+
expect(openURL).toHaveBeenCalledTimes(1)
195+
expect(openURL).toHaveBeenCalledWith(urls.local)
196+
})
197+
198+
test('allows keypresses after debounce period expires', () => {
199+
// Given
200+
const handler = createKeypressHandler(urls, ctx)
201+
202+
// When
203+
handler('t', {name: 't'})
204+
expect(openURL).toHaveBeenCalledTimes(1)
205+
206+
handler('t', {name: 't'})
207+
handler('t', {name: 't'})
208+
expect(openURL).toHaveBeenCalledTimes(1)
209+
210+
// Advance time to exceed debounce period
211+
vi.advanceTimersByTime(100)
212+
213+
handler('p', {name: 'p'})
214+
215+
// Then
216+
expect(openURL).toHaveBeenCalledTimes(2)
217+
expect(openURL).toHaveBeenNthCalledWith(1, urls.local)
218+
expect(openURL).toHaveBeenNthCalledWith(2, urls.preview)
219+
})
220+
221+
test('debounces different keys during the same debounce window', () => {
222+
// Given
223+
const handler = createKeypressHandler(urls, ctx)
224+
225+
// When
226+
handler('t', {name: 't'})
227+
handler('p', {name: 'p'})
228+
handler('e', {name: 'e'})
229+
handler('g', {name: 'g'})
230+
231+
// Then
232+
expect(openURL).toHaveBeenCalledTimes(1)
233+
expect(openURL).toHaveBeenCalledWith(urls.local)
234+
})
235+
})

packages/theme/src/cli/services/dev.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,11 @@ export async function dev(options: DevOptions) {
126126
process.stdin.setRawMode(true)
127127
}
128128

129+
let keyBlock = false
130+
129131
process.stdin.on('keypress', (_str, key) => {
132+
if (keyBlock) return
133+
130134
if (key.ctrl && key.name === 'c') {
131135
process.exit()
132136
}
@@ -150,6 +154,12 @@ export async function dev(options: DevOptions) {
150154
openURLSafely(urls.giftCard, 'gift card preview')
151155
break
152156
}
157+
158+
// This prevents the keypress handler from being called multiple times in a short span
159+
keyBlock = true
160+
setTimeout(() => {
161+
keyBlock = false
162+
}, 100)
153163
})
154164

155165
await Promise.all([
@@ -165,6 +175,48 @@ export async function dev(options: DevOptions) {
165175
])
166176
}
167177

178+
export function createKeypressHandler(
179+
urls: {local: string; giftCard: string; themeEditor: string; preview: string},
180+
ctx: {lastRequestedPath: string},
181+
) {
182+
let keyBlock = false
183+
184+
return (_str: string, key: {ctrl?: boolean; name?: string}) => {
185+
if (keyBlock) return
186+
187+
if (key.ctrl && key.name === 'c') {
188+
process.exit()
189+
}
190+
191+
switch (key.name) {
192+
case 't':
193+
openURLSafely(urls.local, 'localhost')
194+
break
195+
case 'p':
196+
openURLSafely(urls.preview, 'theme preview')
197+
break
198+
case 'e':
199+
openURLSafely(
200+
ctx.lastRequestedPath === '/'
201+
? urls.themeEditor
202+
: `${urls.themeEditor}&previewPath=${encodeURIComponent(ctx.lastRequestedPath)}`,
203+
'theme editor',
204+
)
205+
break
206+
case 'g':
207+
openURLSafely(urls.giftCard, 'gift card preview')
208+
break
209+
default:
210+
return
211+
}
212+
213+
keyBlock = true
214+
setTimeout(() => {
215+
keyBlock = false
216+
}, 100)
217+
}
218+
}
219+
168220
export function openURLSafely(url: string, label: string) {
169221
openURL(url).catch(handleOpenURLError(label))
170222
}

0 commit comments

Comments
 (0)