Skip to content

Commit cd9753f

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 cd9753f

File tree

3 files changed

+184
-2
lines changed

3 files changed

+184
-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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {Theme} from '@shopify/cli-kit/node/themes/types'
1212
import {checkPortAvailability, getAvailableTCPPort} from '@shopify/cli-kit/node/tcp'
1313
import {AbortError} from '@shopify/cli-kit/node/error'
1414
import {openURL} from '@shopify/cli-kit/node/system'
15+
import {debounce} from '@shopify/cli-kit/common/function'
1516
import chalk from '@shopify/cli-kit/node/colors'
1617
import readline from 'readline'
1718

@@ -126,7 +127,11 @@ export async function dev(options: DevOptions) {
126127
process.stdin.setRawMode(true)
127128
}
128129

130+
let keyBlock = false
131+
129132
process.stdin.on('keypress', (_str, key) => {
133+
if (keyBlock) return
134+
130135
if (key.ctrl && key.name === 'c') {
131136
process.exit()
132137
}
@@ -150,6 +155,12 @@ export async function dev(options: DevOptions) {
150155
openURLSafely(urls.giftCard, 'gift card preview')
151156
break
152157
}
158+
159+
// This prevents the keypress handler from being called multiple times in a short span
160+
keyBlock = true
161+
setTimeout(() => {
162+
keyBlock = false
163+
}, 100)
153164
})
154165

155166
await Promise.all([
@@ -165,6 +176,41 @@ export async function dev(options: DevOptions) {
165176
])
166177
}
167178

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

0 commit comments

Comments
 (0)