mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-19 16:21:13 +01:00
Add hotkey dialog and rust-only analytics
This commit is contained in:
@@ -2,7 +2,6 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
@@ -13,7 +12,6 @@ import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { responsesQueryKey } from '../hooks/useResponses';
|
||||
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import { trackPage } from '../lib/analytics';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
||||
import { modelsEq } from '../lib/models';
|
||||
@@ -39,10 +37,6 @@ export function GlobalHooks() {
|
||||
setPathname(location.pathname).catch(console.error);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
trackPage('/');
|
||||
});
|
||||
|
||||
useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
|
||||
if (shouldIgnoreEvent(payload, windowLabel)) return;
|
||||
|
||||
|
||||
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { hotkeyActions } from '../hooks/useHotkey';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
|
||||
export const KeyboardShortcutsDialog = () => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<HotKeyList hotkeys={hotkeyActions} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||
|
||||
export function SettingsDropdown() {
|
||||
const importData = useImportData();
|
||||
@@ -66,7 +67,18 @@ export function SettingsDropdown() {
|
||||
onSelect: toggleAppearance,
|
||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||
},
|
||||
{ type: 'separator', label: `v${appVersion.data}` },
|
||||
{
|
||||
key: 'hotkeys',
|
||||
label: 'Keyboard shortcuts',
|
||||
onSelect: () =>
|
||||
dialog.show({
|
||||
title: 'Keyboard Shortcuts',
|
||||
size: 'dynamic',
|
||||
render: () => <KeyboardShortcutsDialog />,
|
||||
}),
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appVersion.data}` },
|
||||
{
|
||||
key: 'update-mode',
|
||||
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
||||
|
||||
@@ -290,27 +290,13 @@ function getExtensions({
|
||||
|
||||
// Handle onChange
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
|
||||
if (onChange && update.docChanged) {
|
||||
onChange.current?.(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
|
||||
// Make sure document has changed, ensuring user events like selections don't count.
|
||||
if (viewUpdate.docChanged) {
|
||||
// Check transactions for any that are direct user input, not changes from Y.js or another extension.
|
||||
for (const transaction of viewUpdate.transactions) {
|
||||
// Not using Transaction.isUserEvent because that only checks for a specific User event type ( "input", "delete", etc.). Checking the annotation directly allows for any type of user event.
|
||||
const userEventType = transaction.annotation(Transaction.userEvent);
|
||||
if (userEventType) return userEventType;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const syncGutterBg = ({
|
||||
parent,
|
||||
className = '',
|
||||
|
||||
@@ -26,6 +26,7 @@ const icons = {
|
||||
gear: I.GearIcon,
|
||||
hamburger: I.HamburgerMenuIcon,
|
||||
home: I.HomeIcon,
|
||||
keyboard: I.KeyboardIcon,
|
||||
listBullet: I.ListBulletIcon,
|
||||
magicWand: I.MagicWandIcon,
|
||||
magnifyingGlass: I.MagnifyingGlassIcon,
|
||||
|
||||
@@ -13,8 +13,6 @@ export function useCreateEnvironment() {
|
||||
const prompt = usePrompt();
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const queryClient = useQueryClient();
|
||||
const environments = useEnvironments();
|
||||
const workspaces = useWorkspaces();
|
||||
|
||||
return useMutation<Environment, unknown, void>({
|
||||
mutationFn: async () => {
|
||||
@@ -26,7 +24,7 @@ export function useCreateEnvironment() {
|
||||
});
|
||||
return invoke('create_environment', { name, variables: [], workspaceId });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'create'),
|
||||
onSettled: () => trackEvent('Environment', 'Create'),
|
||||
onSuccess: async (environment) => {
|
||||
if (workspaceId == null) return;
|
||||
routes.setEnvironment(environment);
|
||||
|
||||
@@ -18,7 +18,7 @@ export function useCreateFolder() {
|
||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||
return invoke('create_folder', { workspaceId, ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'create'),
|
||||
onSettled: () => trackEvent('Folder', 'Create'),
|
||||
onSuccess: async (request) => {
|
||||
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId: request.workspaceId }));
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ export function useCreateRequest() {
|
||||
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||
return invoke('create_request', { workspaceId, name: '', ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'create'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Create'),
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
|
||||
@@ -12,7 +12,7 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }
|
||||
mutationFn: (patch) => {
|
||||
return invoke('create_workspace', patch);
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'create'),
|
||||
onSettled: () => trackEvent('Workspace', 'Create'),
|
||||
onSuccess: async (workspace) => {
|
||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) => [
|
||||
...(workspaces ?? []),
|
||||
|
||||
@@ -28,7 +28,7 @@ export function useDeleteAnyRequest() {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_request', { requestId: id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'delete'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Delete'),
|
||||
onSuccess: async (request) => {
|
||||
// Was it cancelled?
|
||||
if (request === null) return;
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useDeleteEnvironment(environment: Environment | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_environment', { environmentId: environment?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'delete'),
|
||||
onSettled: () => trackEvent('Environment', 'Delete'),
|
||||
onSuccess: async (environment) => {
|
||||
if (environment === null) return;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export function useDeleteFolder(id: string | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_folder', { folderId: id });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'delete'),
|
||||
onSettled: () => trackEvent('Folder', 'Delete'),
|
||||
onSuccess: async (folder) => {
|
||||
// Was it cancelled?
|
||||
if (folder === null) return;
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useDeleteResponse(id: string | null) {
|
||||
mutationFn: async () => {
|
||||
return await invoke('delete_response', { id: id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete'),
|
||||
onSettled: () => trackEvent('HttpResponse', 'Delete'),
|
||||
onSuccess: ({ requestId, id: responseId }) => {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) =>
|
||||
(responses ?? []).filter((response) => response.id !== responseId),
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useDeleteResponses(requestId?: string) {
|
||||
if (requestId === undefined) return;
|
||||
await invoke('delete_all_responses', { requestId });
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete_many'),
|
||||
onSettled: () => trackEvent('HttpResponse', 'DeleteMany'),
|
||||
onSuccess: async () => {
|
||||
if (requestId === undefined) return;
|
||||
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_workspace', { workspaceId: workspace?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'delete'),
|
||||
onSettled: () => trackEvent('Workspace', 'Delete'),
|
||||
onSuccess: async (workspace) => {
|
||||
if (workspace === null) return;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useDuplicateRequest({
|
||||
if (id === null) throw new Error("Can't duplicate a null request");
|
||||
return invoke('duplicate_request', { id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'duplicate'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Duplicate'),
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
|
||||
@@ -22,6 +22,8 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'environmentEditor.toggle': ['CmdCtrl+e'],
|
||||
};
|
||||
|
||||
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||
|
||||
interface Options {
|
||||
enable?: boolean;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useSendAnyRequest() {
|
||||
const alert = useAlert();
|
||||
return useMutation<HttpResponse, string, string | null>({
|
||||
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
|
||||
onSettled: () => trackEvent('http_request', 'send'),
|
||||
onSettled: () => trackEvent('HttpRequest', 'Send'),
|
||||
onError: (err) => alert({ title: 'Export Failed', body: err }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,5 @@ export function useSendManyRequests() {
|
||||
sendAnyRequest.mutate(id);
|
||||
}
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'send'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,58 +1,20 @@
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import type { Environment, Folder, HttpRequest, HttpResponse, KeyValue, Workspace } from './models';
|
||||
|
||||
const appVersion = await getVersion();
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
export function trackEvent(
|
||||
resource:
|
||||
| Workspace['model']
|
||||
| Environment['model']
|
||||
| Folder['model']
|
||||
| HttpRequest['model']
|
||||
| HttpResponse['model']
|
||||
| KeyValue['model'],
|
||||
event: 'create' | 'update' | 'delete' | 'delete_many' | 'send' | 'duplicate',
|
||||
| 'App'
|
||||
| 'Workspace'
|
||||
| 'Environment'
|
||||
| 'Folder'
|
||||
| 'HttpRequest'
|
||||
| 'HttpResponse'
|
||||
| 'KeyValue',
|
||||
action: 'Launch' | 'Create' | 'Update' | 'Delete' | 'DeleteMany' | 'Send' | 'Duplicate',
|
||||
attributes: Record<string, string | number> = {},
|
||||
) {
|
||||
send('/e', [
|
||||
{ name: 'e', value: `${resource}.${event}` },
|
||||
{ name: 'a', value: JSON.stringify({ ...attributes, version: appVersion }) },
|
||||
]);
|
||||
}
|
||||
|
||||
export function trackPage(pathname: string) {
|
||||
if (pathname === sessionStorage.lastPathName) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.lastPathName = pathname;
|
||||
send('/p', [
|
||||
{
|
||||
name: 'h',
|
||||
value: 'desktop.yaak.app',
|
||||
},
|
||||
{ name: 'p', value: pathname },
|
||||
]);
|
||||
}
|
||||
|
||||
function send(path: string, params: { name: string; value: string | number }[]) {
|
||||
if (localStorage.disableAnalytics === 'true') {
|
||||
console.log('Analytics disabled', path, params);
|
||||
}
|
||||
|
||||
params.push({ name: 'id', value: 'site_zOK0d7jeBy2TLxFCnZ' });
|
||||
params.push({
|
||||
name: 'tz',
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
});
|
||||
params.push({ name: 'xy', value: screensize() });
|
||||
const qs = params.map((v) => `${v.name}=${encodeURIComponent(v.value)}`).join('&');
|
||||
const url = `https://t.yaak.app/t${path}?${qs}`;
|
||||
fetch(url, { mode: 'no-cors' }).catch((err) => console.log('Error:', err));
|
||||
}
|
||||
|
||||
function screensize() {
|
||||
const w = window.screen.width;
|
||||
const h = window.screen.height;
|
||||
return `${Math.round(w / 100) * 100}x${Math.round(h / 100) * 100}`;
|
||||
invoke('track_event', {
|
||||
resource: resource,
|
||||
action,
|
||||
attributes,
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user