redmine-context 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +449 -0
- package/dist/bundle/index.d.ts +5 -0
- package/dist/bundle/index.js +5 -0
- package/dist/bundle/json.d.ts +90 -0
- package/dist/bundle/json.js +266 -0
- package/dist/bundle/markdown.d.ts +75 -0
- package/dist/bundle/markdown.js +294 -0
- package/dist/bundle/search-list.d.ts +43 -0
- package/dist/bundle/search-list.js +53 -0
- package/dist/bundle/stable-stringify.d.ts +26 -0
- package/dist/bundle/stable-stringify.js +50 -0
- package/dist/cache/contract.d.ts +157 -0
- package/dist/cache/contract.js +0 -0
- package/dist/cache/disk-index.d.ts +82 -0
- package/dist/cache/disk-index.js +220 -0
- package/dist/cache/disk.d.ts +133 -0
- package/dist/cache/disk.js +313 -0
- package/dist/cache/gc.d.ts +78 -0
- package/dist/cache/gc.js +123 -0
- package/dist/cache/get-or-compute.d.ts +36 -0
- package/dist/cache/get-or-compute.js +52 -0
- package/dist/cache/index.d.ts +9 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/keys.d.ts +76 -0
- package/dist/cache/keys.js +78 -0
- package/dist/cache/memory.d.ts +48 -0
- package/dist/cache/memory.js +110 -0
- package/dist/cache-first.d.ts +127 -0
- package/dist/cache-first.js +227 -0
- package/dist/client/errors.d.ts +33 -0
- package/dist/client/errors.js +49 -0
- package/dist/client/http.d.ts +110 -0
- package/dist/client/http.js +207 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/client/issues.d.ts +71 -0
- package/dist/client/issues.js +100 -0
- package/dist/client/search.d.ts +58 -0
- package/dist/client/search.js +81 -0
- package/dist/config/credentials.d.ts +247 -0
- package/dist/config/credentials.js +427 -0
- package/dist/config/doctor.d.ts +123 -0
- package/dist/config/doctor.js +260 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +6 -0
- package/dist/config/keyring.d.ts +96 -0
- package/dist/config/keyring.js +158 -0
- package/dist/config/login.d.ts +97 -0
- package/dist/config/login.js +189 -0
- package/dist/config/settings.d.ts +94 -0
- package/dist/config/settings.js +140 -0
- package/dist/contract.d.ts +173 -0
- package/dist/contract.js +27 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +8 -0
- package/dist/extract/audio-extractor.d.ts +105 -0
- package/dist/extract/audio-extractor.js +156 -0
- package/dist/extract/audio.d.ts +126 -0
- package/dist/extract/audio.js +184 -0
- package/dist/extract/dispatcher.d.ts +132 -0
- package/dist/extract/dispatcher.js +115 -0
- package/dist/extract/download.d.ts +111 -0
- package/dist/extract/download.js +261 -0
- package/dist/extract/duration.d.ts +106 -0
- package/dist/extract/duration.js +148 -0
- package/dist/extract/ffmpeg.d.ts +56 -0
- package/dist/extract/ffmpeg.js +95 -0
- package/dist/extract/gguf.d.ts +137 -0
- package/dist/extract/gguf.js +215 -0
- package/dist/extract/index.d.ts +19 -0
- package/dist/extract/index.js +19 -0
- package/dist/extract/magic.d.ts +80 -0
- package/dist/extract/magic.js +282 -0
- package/dist/extract/ooxml.d.ts +131 -0
- package/dist/extract/ooxml.js +336 -0
- package/dist/extract/pdf.d.ts +147 -0
- package/dist/extract/pdf.js +322 -0
- package/dist/extract/queue.d.ts +167 -0
- package/dist/extract/queue.js +217 -0
- package/dist/extract/subprocess.d.ts +145 -0
- package/dist/extract/subprocess.js +181 -0
- package/dist/extract/tesseract.d.ts +153 -0
- package/dist/extract/tesseract.js +321 -0
- package/dist/extract/video-extractor.d.ts +84 -0
- package/dist/extract/video-extractor.js +89 -0
- package/dist/extract/video.d.ts +198 -0
- package/dist/extract/video.js +418 -0
- package/dist/extract/which.d.ts +63 -0
- package/dist/extract/which.js +72 -0
- package/dist/extract/whisper-extract.d.ts +211 -0
- package/dist/extract/whisper-extract.js +323 -0
- package/dist/extract/whisper.d.ts +50 -0
- package/dist/extract/whisper.js +67 -0
- package/dist/extract/zip.d.ts +50 -0
- package/dist/extract/zip.js +165 -0
- package/dist/extract-issue-attachments.d.ts +82 -0
- package/dist/extract-issue-attachments.js +156 -0
- package/dist/fetch-attachment-text.d.ts +113 -0
- package/dist/fetch-attachment-text.js +155 -0
- package/dist/fetch-issue-bundle.d.ts +81 -0
- package/dist/fetch-issue-bundle.js +93 -0
- package/dist/fetch-issue-search.d.ts +74 -0
- package/dist/fetch-issue-search.js +120 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +67 -0
- package/dist/normalize/collections.d.ts +52 -0
- package/dist/normalize/collections.js +170 -0
- package/dist/normalize/helpers.d.ts +35 -0
- package/dist/normalize/helpers.js +59 -0
- package/dist/normalize/index.d.ts +2 -0
- package/dist/normalize/index.js +2 -0
- package/dist/normalize/issue.d.ts +34 -0
- package/dist/normalize/issue.js +154 -0
- package/dist/surfaces/cli/commands.d.ts +61 -0
- package/dist/surfaces/cli/commands.js +262 -0
- package/dist/surfaces/cli/main.d.ts +32 -0
- package/dist/surfaces/cli/main.js +210 -0
- package/dist/surfaces/cli/prompts.d.ts +66 -0
- package/dist/surfaces/cli/prompts.js +147 -0
- package/dist/surfaces/cli/tty.d.ts +27 -0
- package/dist/surfaces/cli/tty.js +35 -0
- package/dist/surfaces/cli/types.d.ts +39 -0
- package/dist/surfaces/cli/types.js +7 -0
- package/dist/surfaces/mcp/server.d.ts +171 -0
- package/dist/surfaces/mcp/server.js +427 -0
- package/dist/surfaces/tui/app.d.ts +55 -0
- package/dist/surfaces/tui/app.js +180 -0
- package/dist/surfaces/tui/attachment-status.d.ts +79 -0
- package/dist/surfaces/tui/attachment-status.js +113 -0
- package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
- package/dist/surfaces/tui/components/breadcrumb.js +31 -0
- package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
- package/dist/surfaces/tui/components/gradient-text.js +75 -0
- package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
- package/dist/surfaces/tui/components/scroll-view.js +77 -0
- package/dist/surfaces/tui/components/spinner.d.ts +12 -0
- package/dist/surfaces/tui/components/spinner.js +39 -0
- package/dist/surfaces/tui/components/text-input.d.ts +45 -0
- package/dist/surfaces/tui/components/text-input.js +114 -0
- package/dist/surfaces/tui/format-file-size.d.ts +24 -0
- package/dist/surfaces/tui/format-file-size.js +43 -0
- package/dist/surfaces/tui/glyphs.d.ts +47 -0
- package/dist/surfaces/tui/glyphs.js +84 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
- package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
- package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
- package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
- package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
- package/dist/surfaces/tui/index.d.ts +50 -0
- package/dist/surfaces/tui/index.js +158 -0
- package/dist/surfaces/tui/instance.d.ts +37 -0
- package/dist/surfaces/tui/instance.js +36 -0
- package/dist/surfaces/tui/job-registry.d.ts +113 -0
- package/dist/surfaces/tui/job-registry.js +123 -0
- package/dist/surfaces/tui/job-status.d.ts +45 -0
- package/dist/surfaces/tui/job-status.js +81 -0
- package/dist/surfaces/tui/navigation.d.ts +74 -0
- package/dist/surfaces/tui/navigation.js +87 -0
- package/dist/surfaces/tui/palettes.d.ts +38 -0
- package/dist/surfaces/tui/palettes.js +244 -0
- package/dist/surfaces/tui/screen.d.ts +30 -0
- package/dist/surfaces/tui/screen.js +51 -0
- package/dist/surfaces/tui/screens/about.d.ts +2 -0
- package/dist/surfaces/tui/screens/about.js +35 -0
- package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
- package/dist/surfaces/tui/screens/appearance.js +74 -0
- package/dist/surfaces/tui/screens/config.d.ts +2 -0
- package/dist/surfaces/tui/screens/config.js +82 -0
- package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
- package/dist/surfaces/tui/screens/doctor.js +109 -0
- package/dist/surfaces/tui/screens/export.d.ts +2 -0
- package/dist/surfaces/tui/screens/export.js +168 -0
- package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
- package/dist/surfaces/tui/screens/home-selection.js +80 -0
- package/dist/surfaces/tui/screens/home.d.ts +2 -0
- package/dist/surfaces/tui/screens/home.js +200 -0
- package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
- package/dist/surfaces/tui/screens/issue-detail.js +182 -0
- package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
- package/dist/surfaces/tui/screens/jobs.js +89 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
- package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
- package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
- package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
- package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
- package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
- package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
- package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
- package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
- package/dist/surfaces/tui/screens/welcome.js +95 -0
- package/dist/surfaces/tui/status-color.d.ts +21 -0
- package/dist/surfaces/tui/status-color.js +23 -0
- package/dist/surfaces/tui/symbols.d.ts +231 -0
- package/dist/surfaces/tui/symbols.js +14 -0
- package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
- package/dist/surfaces/tui/terminal-colors.js +39 -0
- package/dist/surfaces/tui/theme.d.ts +156 -0
- package/dist/surfaces/tui/theme.js +86 -0
- package/dist/surfaces/tui/truncate.d.ts +31 -0
- package/dist/surfaces/tui/truncate.js +81 -0
- package/package.json +93 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lógica de dados da busca inline da home (M2-07, issue #30): busca textual
|
|
3
|
+
* + filtro rápido de status via `fetchIssueSearch` do core — orquestração
|
|
4
|
+
* que já combina filtros estruturados (`assigned_to_id=me` + `status_id`)
|
|
5
|
+
* com full-text best-effort (`/search.json`, com degradação sinalizada no
|
|
6
|
+
* payload quando indisponível). Extraído para um hook próprio (em vez de
|
|
7
|
+
* estender `use-my-issues.ts`) porque o gatilho é bem diferente: `useMyIssues`
|
|
8
|
+
* busca uma vez ao montar, enquanto esta busca é retriggada a cada mudança de
|
|
9
|
+
* `query`/`statusFilter` — mas reaproveita deliberadamente o MESMO formato de
|
|
10
|
+
* `MyIssuesState` (`loading`/`error-network`/`error-forbidden`/`auth-aborted`
|
|
11
|
+
* + um estado "carregado") para a tela (`../screens/home.tsx`) tratar os dois
|
|
12
|
+
* hooks de forma consistente.
|
|
13
|
+
*
|
|
14
|
+
* Fronteira do core (ADR-005): a única coisa importada daqui é
|
|
15
|
+
* `../../../index.js` (`fetchIssueSearch`/`resolveApiKey`) — a tela só
|
|
16
|
+
* formata os estados, nunca chama o core diretamente.
|
|
17
|
+
*
|
|
18
|
+
* Debounce de digitação: `query` normalmente muda a cada tecla (controlado
|
|
19
|
+
* pelo `TextInput` da home); disparar `fetchIssueSearch` a cada tecla geraria
|
|
20
|
+
* uma chamada de rede por caractere digitado. Este hook espera
|
|
21
|
+
* `debounceMs` (300ms em produção, {@link DEFAULT_DEBOUNCE_MS}) de silêncio
|
|
22
|
+
* antes de efetivamente buscar — implementado com um `setTimeout` reagendado
|
|
23
|
+
* a cada mudança de `query` (cancelado no cleanup do efeito).
|
|
24
|
+
*
|
|
25
|
+
* 401 via `useAuthGuard` (M2-13, #36, mandatório para qualquer chamada
|
|
26
|
+
* autenticada ao core): a busca é envolvida por `guard()`, mesmo padrão de
|
|
27
|
+
* `use-my-issues.ts` — em 401, o estado permanece `loading` durante o
|
|
28
|
+
* re-auth e a busca é refeita automaticamente após o re-login. Abandono do
|
|
29
|
+
* re-login (Esc) vira o estado `auth-aborted`.
|
|
30
|
+
*/
|
|
31
|
+
import { useEffect, useRef, useState } from 'react';
|
|
32
|
+
import { fetchIssueSearch, resolveApiKey, RedmineForbiddenError, } from '../../../index.js';
|
|
33
|
+
import { useEnvFallbackAllowed } from '../instance.js';
|
|
34
|
+
import { ReAuthAbortedError, useAuthGuard } from './use-auth-guard.js';
|
|
35
|
+
/** Debounce (ms) default aplicado a `query` antes de disparar a busca. */
|
|
36
|
+
export const DEFAULT_DEBOUNCE_MS = 300;
|
|
37
|
+
/** Mapeia o filtro rápido de status para o `status_id` de `/issues.json` (`*` = todas). */
|
|
38
|
+
function statusIdFor(filter) {
|
|
39
|
+
if (filter === 'open')
|
|
40
|
+
return 'open';
|
|
41
|
+
if (filter === 'closed')
|
|
42
|
+
return 'closed';
|
|
43
|
+
return '*';
|
|
44
|
+
}
|
|
45
|
+
/** Lê `REDMINE_URL` do ambiente, tratando string vazia como ausente (mesma convenção do doctor/home). */
|
|
46
|
+
function instanceFromEnv(env) {
|
|
47
|
+
const value = env.REDMINE_URL;
|
|
48
|
+
return value !== undefined && value.length > 0 ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Busca issues (filtros + full-text best-effort) via o core, com debounce de
|
|
52
|
+
* digitação e o filtro rápido de status.
|
|
53
|
+
*
|
|
54
|
+
* @param query - Termo full-text bruto (não debounced) — tipicamente o
|
|
55
|
+
* `value` do `TextInput` da busca da home.
|
|
56
|
+
* @param statusFilter - Filtro rápido de status (ver {@link SearchStatusFilter}).
|
|
57
|
+
* @param options - Dependências injetáveis. Ver {@link UseIssueSearchOptions}.
|
|
58
|
+
* @returns O {@link UseIssueSearchResult} atual.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* const { state, clear } = useIssueSearch(query, statusFilter); // deps de produção
|
|
62
|
+
*/
|
|
63
|
+
export function useIssueSearch(query, statusFilter, options = {}) {
|
|
64
|
+
const env = options.env ?? process.env;
|
|
65
|
+
const resolve = options.resolveApiKey ?? resolveApiKey;
|
|
66
|
+
const runSearch = options.fetchIssueSearch ?? fetchIssueSearch;
|
|
67
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
68
|
+
// Fix do padrão #120 (use-my-issues.ts): qualquer chamada autenticada ao
|
|
69
|
+
// core passa por `guard()` — 401 dispara o re-login e retoma a busca.
|
|
70
|
+
const { guard } = useAuthGuard();
|
|
71
|
+
// SEGURANÇA (#187): origem config (URL persistida) → sem env-key instance-agnóstica.
|
|
72
|
+
const allowEnvFallback = useEnvFallbackAllowed();
|
|
73
|
+
const [state, setState] = useState({ status: 'idle' });
|
|
74
|
+
const [debouncedQuery, setDebouncedQuery] = useState(query);
|
|
75
|
+
// Debounce: só propaga `query` para `debouncedQuery` depois de `debounceMs`
|
|
76
|
+
// de silêncio — reagendado a cada tecla (cleanup cancela o timer anterior).
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const timer = setTimeout(() => {
|
|
79
|
+
setDebouncedQuery(query);
|
|
80
|
+
}, debounceMs);
|
|
81
|
+
return () => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
};
|
|
84
|
+
}, [query, debounceMs]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
let cancelled = false;
|
|
87
|
+
const trimmed = debouncedQuery.trim();
|
|
88
|
+
// Nada para buscar: sem texto E sem filtro de status ativo. `idle` evita
|
|
89
|
+
// uma chamada de rede assim que o campo de busca abre vazio.
|
|
90
|
+
if (trimmed.length === 0 && statusFilter === 'all') {
|
|
91
|
+
setState({ status: 'idle' });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
setState({ status: 'loading' });
|
|
95
|
+
const instanceUrl = instanceFromEnv(env);
|
|
96
|
+
if (instanceUrl === undefined) {
|
|
97
|
+
setState({
|
|
98
|
+
status: 'error-network',
|
|
99
|
+
message: 'Instância não configurada (defina REDMINE_URL).',
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const cascadeOptions = { env, allowEnvFallback };
|
|
104
|
+
void (async () => {
|
|
105
|
+
try {
|
|
106
|
+
const apiKey = await resolve(instanceUrl, cascadeOptions);
|
|
107
|
+
if (apiKey === undefined) {
|
|
108
|
+
if (!cancelled) {
|
|
109
|
+
setState({
|
|
110
|
+
status: 'error-network',
|
|
111
|
+
message: `Nenhuma credencial encontrada para ${instanceUrl}.`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const result = await guard(() => runSearch({
|
|
117
|
+
baseUrl: instanceUrl,
|
|
118
|
+
apiKey,
|
|
119
|
+
filters: { assigned_to_id: 'me', status_id: statusIdFor(statusFilter) },
|
|
120
|
+
query: trimmed.length > 0 ? trimmed : undefined,
|
|
121
|
+
}));
|
|
122
|
+
if (cancelled)
|
|
123
|
+
return;
|
|
124
|
+
setState({
|
|
125
|
+
status: 'loaded',
|
|
126
|
+
content: result.content,
|
|
127
|
+
count: result.count,
|
|
128
|
+
degraded: result.degraded,
|
|
129
|
+
warnings: result.warnings,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (cause) {
|
|
133
|
+
if (cancelled)
|
|
134
|
+
return;
|
|
135
|
+
if (cause instanceof RedmineForbiddenError) {
|
|
136
|
+
setState({
|
|
137
|
+
status: 'error-forbidden',
|
|
138
|
+
message: 'Sem permissão para buscar issues nesta instância (403).',
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (cause instanceof ReAuthAbortedError) {
|
|
143
|
+
setState({
|
|
144
|
+
status: 'auth-aborted',
|
|
145
|
+
message: 'login cancelado — pressione f/digite de novo para tentar',
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
150
|
+
setState({ status: 'error-network', message });
|
|
151
|
+
}
|
|
152
|
+
})();
|
|
153
|
+
return () => {
|
|
154
|
+
cancelled = true;
|
|
155
|
+
};
|
|
156
|
+
// Reason: `env`/`resolve`/`runSearch`/`guard` são estáveis entre renders
|
|
157
|
+
// com as deps de produção (defaults do módulo, ou `process.env`, ou a
|
|
158
|
+
// identidade estável de `useAuthGuard().guard`) — o efeito só precisa
|
|
159
|
+
// refazer a busca quando `debouncedQuery`/`statusFilter` mudam.
|
|
160
|
+
}, [debouncedQuery, statusFilter, env, allowEnvFallback, resolve, runSearch, guard]);
|
|
161
|
+
// Handler ESTÁVEL (useCallback via ref indireta não é necessário aqui: sem
|
|
162
|
+
// dependências externas mutáveis, `clear` já é estável por construção).
|
|
163
|
+
const clear = useRef(() => {
|
|
164
|
+
setDebouncedQuery('');
|
|
165
|
+
setState({ status: 'idle' });
|
|
166
|
+
}).current;
|
|
167
|
+
return { state, clear };
|
|
168
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Dispatch, type SetStateAction } from 'react';
|
|
2
|
+
/** Opções do hook — todas opcionais. */
|
|
3
|
+
export interface UseListNavigationOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Chamado com o índice atual quando o usuário confirma a seleção (Enter).
|
|
6
|
+
* Opcional: uma lista somente de exibição pode omitir e ler apenas
|
|
7
|
+
* `selectedIndex`.
|
|
8
|
+
*/
|
|
9
|
+
onSelect?: (index: number) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Desliga a captura de teclado deste hook quando `false` (mesmo padrão de
|
|
12
|
+
* `isActive` do `../components/text-input.tsx`) — necessário quando outro
|
|
13
|
+
* campo da mesma tela está temporariamente "dono" do teclado (ex.: a busca
|
|
14
|
+
* inline da home, M2-07/#30). Default: `true`.
|
|
15
|
+
*/
|
|
16
|
+
isActive?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Cursor inicial — default `0`. Aplicado assim que `itemCount` deixa de
|
|
19
|
+
* ser `0` pela primeira vez (clampado); mudanças subsequentes NÃO
|
|
20
|
+
* reposicionam o cursor (preservação de seleção entre remounts — #31).
|
|
21
|
+
*/
|
|
22
|
+
initialIndex?: number;
|
|
23
|
+
}
|
|
24
|
+
/** Valor retornado pelo hook. */
|
|
25
|
+
export interface UseListNavigationResult {
|
|
26
|
+
/** Índice selecionado (0-based), sempre dentro de `[0, itemCount - 1]`. */
|
|
27
|
+
selectedIndex: number;
|
|
28
|
+
/** Setter direto — útil para resetar a seleção quando a lista troca de conteúdo (ex.: novo filtro). */
|
|
29
|
+
setSelectedIndex: Dispatch<SetStateAction<number>>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Hook de navegação em lista: setas/`j`/`k` movem `selectedIndex` com wrap,
|
|
33
|
+
* Enter confirma via `onSelect`.
|
|
34
|
+
*
|
|
35
|
+
* @param itemCount Total de itens navegáveis. `0` desativa a navegação (sem
|
|
36
|
+
* quebrar — apenas nada acontece, a lista está vazia).
|
|
37
|
+
* @param options Ver {@link UseListNavigationOptions}.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const { selectedIndex } = useListNavigation(issues.length, {
|
|
41
|
+
* onSelect: (index) => push('issue-detail'),
|
|
42
|
+
* });
|
|
43
|
+
*/
|
|
44
|
+
export declare function useListNavigation(itemCount: number, options?: UseListNavigationOptions): UseListNavigationResult;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Padrão reutilizável de navegação em listas da TUI (M2-04).
|
|
3
|
+
*
|
|
4
|
+
* Encapsula o `useInput()` comum a toda lista selecionável: setas ↑/↓ (ou
|
|
5
|
+
* `j`/`k`, atalhos vim-like opcionais) movem `selectedIndex` com wrap nas
|
|
6
|
+
* pontas; Enter confirma via `onSelect`. Cada tela de lista (M2-06 é a
|
|
7
|
+
* primeira a consumir isso) só precisa chamar o hook com o total de itens —
|
|
8
|
+
* nenhuma tela reimplementa esse teclado do zero.
|
|
9
|
+
*
|
|
10
|
+
* Esc (voltar) e Ctrl+C (sair) NÃO são tratados aqui — são atalhos globais
|
|
11
|
+
* do roteador (`../app.tsx`, `use-exit-guard.ts`), fora do escopo de uma
|
|
12
|
+
* lista específica.
|
|
13
|
+
*/
|
|
14
|
+
import { useInput } from 'ink';
|
|
15
|
+
import { useEffect, useRef, useState } from 'react';
|
|
16
|
+
/**
|
|
17
|
+
* Hook de navegação em lista: setas/`j`/`k` movem `selectedIndex` com wrap,
|
|
18
|
+
* Enter confirma via `onSelect`.
|
|
19
|
+
*
|
|
20
|
+
* @param itemCount Total de itens navegáveis. `0` desativa a navegação (sem
|
|
21
|
+
* quebrar — apenas nada acontece, a lista está vazia).
|
|
22
|
+
* @param options Ver {@link UseListNavigationOptions}.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const { selectedIndex } = useListNavigation(issues.length, {
|
|
26
|
+
* onSelect: (index) => push('issue-detail'),
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
export function useListNavigation(itemCount, options = {}) {
|
|
30
|
+
const { onSelect, isActive = true, initialIndex } = options;
|
|
31
|
+
// Ref (não dep de efeito): a aplicação do `initialIndex` deve acontecer uma
|
|
32
|
+
// única vez, na primeira vez que a lista deixa de estar vazia — mesmo que
|
|
33
|
+
// `initialIndex` mude entre renders (ex.: o contexto de origem re-renderiza
|
|
34
|
+
// por outro motivo).
|
|
35
|
+
const appliedInitialRef = useRef(false);
|
|
36
|
+
const [selectedIndex, setSelectedIndex] = useState(() => {
|
|
37
|
+
if (itemCount <= 0) {
|
|
38
|
+
// Nada para clampar ainda — o efeito abaixo aplica `initialIndex` assim
|
|
39
|
+
// que `itemCount` deixar de ser 0 (ex.: lista ainda carregando).
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
appliedInitialRef.current = true;
|
|
43
|
+
return Math.min(Math.max(initialIndex ?? 0, 0), itemCount - 1);
|
|
44
|
+
});
|
|
45
|
+
useInput((input, key) => {
|
|
46
|
+
if (itemCount <= 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (key.upArrow || input === 'k') {
|
|
50
|
+
setSelectedIndex((current) => (current - 1 + itemCount) % itemCount);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (key.downArrow || input === 'j') {
|
|
54
|
+
setSelectedIndex((current) => (current + 1) % itemCount);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.return) {
|
|
58
|
+
onSelect?.(selectedIndex);
|
|
59
|
+
}
|
|
60
|
+
}, { isActive });
|
|
61
|
+
// Reason: mantém `selectedIndex` dentro dos limites quando `itemCount`
|
|
62
|
+
// encolhe (ex.: um filtro reduz a lista) — evita um índice "fantasma" que
|
|
63
|
+
// aponta para fora do array de itens da tela. Também é o gatilho de
|
|
64
|
+
// aplicação do `initialIndex` (#31): na primeira vez que `itemCount` deixa
|
|
65
|
+
// de ser 0, usa `initialIndex` (se informado) como base em vez do cursor
|
|
66
|
+
// atual — cobre tanto o caso síncrono (lista já não-vazia no primeiro
|
|
67
|
+
// render) quanto o assíncrono (lista carrega depois, ex.: `home.tsx`).
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setSelectedIndex((current) => {
|
|
70
|
+
if (itemCount <= 0) {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
const base = !appliedInitialRef.current && initialIndex !== undefined ? initialIndex : current;
|
|
74
|
+
appliedInitialRef.current = true;
|
|
75
|
+
return Math.min(Math.max(base, 0), itemCount - 1);
|
|
76
|
+
});
|
|
77
|
+
// Reason: `initialIndex` é intencionalmente OMITIDO das deps — é lido só
|
|
78
|
+
// na aplicação única controlada por `appliedInitialRef`, nunca deve
|
|
79
|
+
// reexecutar este efeito por si só (ver o JSDoc da opção).
|
|
80
|
+
}, [itemCount]);
|
|
81
|
+
return { selectedIndex, setSelectedIndex };
|
|
82
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { diagnoseBinaries, type BinaryDiagnosis } from '../../../index.js';
|
|
2
|
+
/** Estado da seção "Binários de mídia": carregando + o resultado do diagnóstico. */
|
|
3
|
+
export interface MediaBinariesState {
|
|
4
|
+
/** `true` enquanto o diagnóstico assíncrono ainda não resolveu. */
|
|
5
|
+
loading: boolean;
|
|
6
|
+
/** Binários diagnosticados (vazio até resolver, ou se o diagnóstico falhar). */
|
|
7
|
+
binaries: BinaryDiagnosis[];
|
|
8
|
+
}
|
|
9
|
+
/** Dependências injetáveis do hook — todas opcionais, com defaults de produção. */
|
|
10
|
+
export interface UseMediaBinariesOptions {
|
|
11
|
+
/** Executa o diagnóstico; default `diagnoseBinaries` do core. */
|
|
12
|
+
diagnose?: typeof diagnoseBinaries;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Diagnostica os binários de mídia uma vez, no mount. Nunca lança: se o
|
|
16
|
+
* diagnóstico rejeitar, sai do loading com uma lista vazia (a tela apenas não
|
|
17
|
+
* exibe binários — degradação graciosa).
|
|
18
|
+
*
|
|
19
|
+
* @param options - Deps injetáveis. Ver {@link UseMediaBinariesOptions}.
|
|
20
|
+
* @returns O {@link MediaBinariesState} atual — começa em `loading: true`.
|
|
21
|
+
* @example
|
|
22
|
+
* const { loading, binaries } = useMediaBinaries();
|
|
23
|
+
*/
|
|
24
|
+
export declare function useMediaBinaries(options?: UseMediaBinariesOptions): MediaBinariesState;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lógica assíncrona da seção "Binários de mídia" da tela doctor (M3-11, #53).
|
|
3
|
+
*
|
|
4
|
+
* Extraída para um hook porque o diagnóstico (`diagnoseBinaries`) é assíncrono
|
|
5
|
+
* (localiza binários no filesystem e lê versão via subprocesso) e precisa ser
|
|
6
|
+
* testável com a dependência injetada — nenhum teste deste arquivo consulta um
|
|
7
|
+
* binário real.
|
|
8
|
+
*
|
|
9
|
+
* Fronteira do core (ADR-005): a única coisa importada daqui é `diagnoseBinaries`
|
|
10
|
+
* via `../../../index.js` — a mesma superfície pública que a CLI consome para o
|
|
11
|
+
* comando `doctor`. Sem reimplementar detecção na TUI.
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useState } from 'react';
|
|
14
|
+
import { diagnoseBinaries } from '../../../index.js';
|
|
15
|
+
/**
|
|
16
|
+
* Diagnostica os binários de mídia uma vez, no mount. Nunca lança: se o
|
|
17
|
+
* diagnóstico rejeitar, sai do loading com uma lista vazia (a tela apenas não
|
|
18
|
+
* exibe binários — degradação graciosa).
|
|
19
|
+
*
|
|
20
|
+
* @param options - Deps injetáveis. Ver {@link UseMediaBinariesOptions}.
|
|
21
|
+
* @returns O {@link MediaBinariesState} atual — começa em `loading: true`.
|
|
22
|
+
* @example
|
|
23
|
+
* const { loading, binaries } = useMediaBinaries();
|
|
24
|
+
*/
|
|
25
|
+
export function useMediaBinaries(options = {}) {
|
|
26
|
+
const diagnose = options.diagnose ?? diagnoseBinaries;
|
|
27
|
+
const [state, setState] = useState({ loading: true, binaries: [] });
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let cancelled = false;
|
|
30
|
+
diagnose().then((binaries) => {
|
|
31
|
+
if (!cancelled)
|
|
32
|
+
setState({ loading: false, binaries });
|
|
33
|
+
}, () => {
|
|
34
|
+
if (!cancelled)
|
|
35
|
+
setState({ loading: false, binaries: [] });
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
};
|
|
40
|
+
// Reason: `diagnose` é estável entre renders (default do módulo, ou uma dep
|
|
41
|
+
// injetada estável no teste) — o diagnóstico só precisa rodar uma vez.
|
|
42
|
+
}, [diagnose]);
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHttpClient, listIssues, resolveApiKey } from '../../../index.js';
|
|
2
|
+
/** Issue resumida exibida numa linha da lista da home — só os campos usados por `IssueRow`. */
|
|
3
|
+
export interface MyIssue {
|
|
4
|
+
/** Id numérico da issue (`#id` na linha). */
|
|
5
|
+
id: number;
|
|
6
|
+
/** Assunto da issue (truncado na exibição, nunca aqui). */
|
|
7
|
+
subject: string;
|
|
8
|
+
/** Nome do status (badge colorido por tema). */
|
|
9
|
+
statusName: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Estado da busca de "minhas issues", consumido por `../screens/home.tsx`.
|
|
13
|
+
* `loading`/`empty`/`error-network`/`error-forbidden` são os 4 estados
|
|
14
|
+
* visuais distintos exigidos pela AC da #29; `loaded` é o estado "normal"
|
|
15
|
+
* com a lista preenchida. `auth-aborted` (fix do review #120) é um 5º
|
|
16
|
+
* estado leve, distinto de `error-network`: o usuário viu o prompt de
|
|
17
|
+
* re-login (401 tratado por `useAuthGuard`) e escolheu conscientemente (Esc)
|
|
18
|
+
* não reautenticar — não é uma falha inesperada, então não usa o vocabulário
|
|
19
|
+
* de erro; `retry` (tecla `r`) segue disponível para tentar de novo.
|
|
20
|
+
*/
|
|
21
|
+
export type MyIssuesState = {
|
|
22
|
+
status: 'loading';
|
|
23
|
+
} | {
|
|
24
|
+
status: 'empty';
|
|
25
|
+
} | {
|
|
26
|
+
status: 'loaded';
|
|
27
|
+
issues: MyIssue[];
|
|
28
|
+
} | {
|
|
29
|
+
status: 'error-network';
|
|
30
|
+
message: string;
|
|
31
|
+
} | {
|
|
32
|
+
status: 'error-forbidden';
|
|
33
|
+
message: string;
|
|
34
|
+
} | {
|
|
35
|
+
status: 'auth-aborted';
|
|
36
|
+
message: string;
|
|
37
|
+
};
|
|
38
|
+
/** Dependências injetáveis do hook — todas opcionais, com defaults de produção via o core. */
|
|
39
|
+
export interface UseMyIssuesOptions {
|
|
40
|
+
/** Ambiente consultado para `REDMINE_URL`; default `process.env`. */
|
|
41
|
+
env?: NodeJS.ProcessEnv;
|
|
42
|
+
/** Resolve a api_key pela cascata M2; default `resolveApiKey` do core. */
|
|
43
|
+
resolveApiKey?: typeof resolveApiKey;
|
|
44
|
+
/** Constrói o client HTTP autenticado; default `createHttpClient` do core. */
|
|
45
|
+
createHttpClient?: typeof createHttpClient;
|
|
46
|
+
/** Lista issues paginando `/issues.json`; default `listIssues` do core. */
|
|
47
|
+
listIssues?: typeof listIssues;
|
|
48
|
+
}
|
|
49
|
+
/** Valor retornado pelo hook. */
|
|
50
|
+
export interface UseMyIssuesResult {
|
|
51
|
+
/** Estado atual da busca (ver {@link MyIssuesState}). */
|
|
52
|
+
state: MyIssuesState;
|
|
53
|
+
/** Reexecuta a busca do zero (`status` volta a `loading`) — usado pelo retry (tecla `r`) dos estados de erro. */
|
|
54
|
+
retry: () => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Lista "minhas issues" (`assigned_to_id=me`) via o core.
|
|
58
|
+
*
|
|
59
|
+
* @param options - Dependências injetáveis. Ver {@link UseMyIssuesOptions}.
|
|
60
|
+
* @returns O {@link UseMyIssuesResult} atual — `state.status` começa em
|
|
61
|
+
* `'loading'` e é atualizado quando a busca assíncrona resolve.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const { state, retry } = useMyIssues(); // deps de produção (process.env, core real)
|
|
65
|
+
*/
|
|
66
|
+
export declare function useMyIssues(options?: UseMyIssuesOptions): UseMyIssuesResult;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lógica de dados da tela home (M2-06, #29): lista "minhas issues"
|
|
3
|
+
* (`assigned_to_id=me`) via o core — resolve a credencial pela cascata
|
|
4
|
+
* (`resolveApiKey`), monta um client HTTP autenticado (`createHttpClient`) e
|
|
5
|
+
* pagina `/issues.json` (`listIssues`). Extraída para um hook porque toda a
|
|
6
|
+
* busca é assíncrona e precisa ser testável com dependências injetadas —
|
|
7
|
+
* nenhum teste deste arquivo toca keychain ou rede reais.
|
|
8
|
+
*
|
|
9
|
+
* Fronteira do core (ADR-005): a única coisa importada daqui é
|
|
10
|
+
* `../../../index.js` — a tela (`../screens/home.tsx`) só formata os 5
|
|
11
|
+
* estados visuais (`loading`/`empty`/`error-network`/`error-forbidden`/
|
|
12
|
+
* `auth-aborted`) mais o estado "com itens" (`loaded`), nunca chama o core
|
|
13
|
+
* diretamente.
|
|
14
|
+
*
|
|
15
|
+
* A instância vem de `REDMINE_URL` (mesma convenção do doctor/CLI/MCP —
|
|
16
|
+
* ver `use-doctor-status.ts`), lida do `env` injetado (default `process.env`).
|
|
17
|
+
*
|
|
18
|
+
* Fix do review do PR #120: esta era a primeira tela de dados (#29+) a
|
|
19
|
+
* chamar o core autenticado SEM passar pelo `useAuthGuard` (M2-13, #36,
|
|
20
|
+
* mandatório para qualquer chamada assim, ver `./use-auth-guard.ts`) — um 401
|
|
21
|
+
* (api_key expirada/revogada) virava um `error-network` cru em vez de
|
|
22
|
+
* disparar o re-login e retomar a busca automaticamente. A busca (`listIssues`)
|
|
23
|
+
* agora é envolvida por `guard()`: em 401, o estado permanece `loading`
|
|
24
|
+
* durante o re-auth (a `Promise` de `guard()` só resolve/rejeita depois que o
|
|
25
|
+
* usuário loga de novo, ver `use-auth-guard.ts`) e a lista carrega assim que o
|
|
26
|
+
* re-login tem sucesso, sem ação extra do usuário. Se o re-login for
|
|
27
|
+
* abandonado (Esc), `guard()` rejeita com `ReAuthAbortedError` — tratado aqui
|
|
28
|
+
* como o estado próprio `auth-aborted` (não um erro cru), com `retry` (tecla
|
|
29
|
+
* `r`, já existente) disponível para tentar de novo.
|
|
30
|
+
*/
|
|
31
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
32
|
+
import { createHttpClient, listIssues, resolveApiKey, RedmineForbiddenError, } from '../../../index.js';
|
|
33
|
+
import { useEnvFallbackAllowed } from '../instance.js';
|
|
34
|
+
import { ReAuthAbortedError, useAuthGuard } from './use-auth-guard.js';
|
|
35
|
+
/** Estreita `unknown` para um objeto indexável, ou `undefined` se não for. */
|
|
36
|
+
function asRecord(value) {
|
|
37
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Converte um payload bruto de issue em {@link MyIssue}; `undefined` se
|
|
41
|
+
* `subject`/`status.name` faltarem ou tiverem um shape inesperado — a linha
|
|
42
|
+
* é simplesmente omitida da lista em vez de quebrar a tela inteira.
|
|
43
|
+
*/
|
|
44
|
+
function toMyIssue(raw) {
|
|
45
|
+
const subject = raw.subject;
|
|
46
|
+
const statusName = asRecord(raw.status)?.name;
|
|
47
|
+
if (typeof subject !== 'string' || typeof statusName !== 'string') {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return { id: raw.id, subject, statusName };
|
|
51
|
+
}
|
|
52
|
+
/** Lê `REDMINE_URL` do ambiente, tratando string vazia como ausente (mesma convenção do doctor). */
|
|
53
|
+
function instanceFromEnv(env) {
|
|
54
|
+
const value = env.REDMINE_URL;
|
|
55
|
+
return value !== undefined && value.length > 0 ? value : undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Lista "minhas issues" (`assigned_to_id=me`) via o core.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Dependências injetáveis. Ver {@link UseMyIssuesOptions}.
|
|
61
|
+
* @returns O {@link UseMyIssuesResult} atual — `state.status` começa em
|
|
62
|
+
* `'loading'` e é atualizado quando a busca assíncrona resolve.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const { state, retry } = useMyIssues(); // deps de produção (process.env, core real)
|
|
66
|
+
*/
|
|
67
|
+
export function useMyIssues(options = {}) {
|
|
68
|
+
const env = options.env ?? process.env;
|
|
69
|
+
const resolve = options.resolveApiKey ?? resolveApiKey;
|
|
70
|
+
const buildClient = options.createHttpClient ?? createHttpClient;
|
|
71
|
+
const fetchIssues = options.listIssues ?? listIssues;
|
|
72
|
+
// Fix do review #120: a busca é uma chamada autenticada ao core — `guard`
|
|
73
|
+
// (identidade estável, `useCallback` sem deps em `use-auth-guard.ts`) trata
|
|
74
|
+
// 401 disparando o re-login e retomando a operação automaticamente.
|
|
75
|
+
const { guard } = useAuthGuard();
|
|
76
|
+
// SEGURANÇA (#187): se a instância veio da persistida (origem config), a
|
|
77
|
+
// REDMINE_API_KEY instance-agnóstica NÃO pode ser usada (só credencial pinada).
|
|
78
|
+
const allowEnvFallback = useEnvFallbackAllowed();
|
|
79
|
+
const [state, setState] = useState({ status: 'loading' });
|
|
80
|
+
const [reloadToken, setReloadToken] = useState(0);
|
|
81
|
+
const retry = useCallback(() => {
|
|
82
|
+
setReloadToken((current) => current + 1);
|
|
83
|
+
}, []);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
setState({ status: 'loading' });
|
|
87
|
+
const instanceUrl = instanceFromEnv(env);
|
|
88
|
+
if (instanceUrl === undefined) {
|
|
89
|
+
setState({
|
|
90
|
+
status: 'error-network',
|
|
91
|
+
message: 'Instância não configurada (defina REDMINE_URL).',
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const cascadeOptions = { env, allowEnvFallback };
|
|
96
|
+
void (async () => {
|
|
97
|
+
try {
|
|
98
|
+
const apiKey = await resolve(instanceUrl, cascadeOptions);
|
|
99
|
+
if (apiKey === undefined) {
|
|
100
|
+
if (!cancelled) {
|
|
101
|
+
setState({
|
|
102
|
+
status: 'error-network',
|
|
103
|
+
message: `Nenhuma credencial encontrada para ${instanceUrl}.`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const http = buildClient({ baseUrl: instanceUrl, apiKey });
|
|
109
|
+
// Fix do review #120: envolvida por `guard()` — em 401, a Promise só
|
|
110
|
+
// resolve após o re-login ter sucesso e a busca ser refeita
|
|
111
|
+
// automaticamente; o estado do hook permanece `loading` enquanto isso.
|
|
112
|
+
const raw = await guard(() => fetchIssues(http, { filters: { assigned_to_id: 'me' } }));
|
|
113
|
+
if (cancelled)
|
|
114
|
+
return;
|
|
115
|
+
const issues = raw.map(toMyIssue).filter((issue) => issue !== undefined);
|
|
116
|
+
setState(issues.length === 0 ? { status: 'empty' } : { status: 'loaded', issues });
|
|
117
|
+
}
|
|
118
|
+
catch (cause) {
|
|
119
|
+
if (cancelled)
|
|
120
|
+
return;
|
|
121
|
+
if (cause instanceof RedmineForbiddenError) {
|
|
122
|
+
setState({
|
|
123
|
+
status: 'error-forbidden',
|
|
124
|
+
message: 'Sem permissão para listar suas issues nesta instância (403).',
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (cause instanceof ReAuthAbortedError) {
|
|
129
|
+
// Fix do review #120: abandono consciente do re-login (Esc) — estado
|
|
130
|
+
// neutro dedicado, não um erro cru. `retry` (tecla `r`, já existente
|
|
131
|
+
// na tela) refaz a busca do zero.
|
|
132
|
+
setState({
|
|
133
|
+
status: 'auth-aborted',
|
|
134
|
+
message: 'login cancelado — pressione r para tentar de novo',
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
139
|
+
setState({ status: 'error-network', message });
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
return () => {
|
|
143
|
+
cancelled = true;
|
|
144
|
+
};
|
|
145
|
+
// Reason: `env`/`resolve`/`buildClient`/`fetchIssues`/`guard` são estáveis
|
|
146
|
+
// entre renders com as deps de produção (defaults do módulo, ou
|
|
147
|
+
// `process.env`, ou a identidade estável de `useAuthGuard().guard`) — o
|
|
148
|
+
// efeito só precisa refazer a busca quando `reloadToken` muda (retry).
|
|
149
|
+
}, [env, allowEnvFallback, resolve, buildClient, fetchIssues, guard, reloadToken]);
|
|
150
|
+
return { state, retry };
|
|
151
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SettingsStore } from '../../../index.js';
|
|
2
|
+
import type { OnboardingCallbacks } from '../screens/onboarding/onboarding-context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Monta os callbacks reais de onboarding — injetados em
|
|
5
|
+
* `<OnboardingProvider callbacks={...}>` por `../app.tsx`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const callbacks = useOnboardingCallbacks();
|
|
9
|
+
* return <OnboardingProvider callbacks={callbacks}>...</OnboardingProvider>;
|
|
10
|
+
*/
|
|
11
|
+
export declare function useOnboardingCallbacks(settings?: SettingsStore): OnboardingCallbacks;
|