feat(client): set dynamic page title based on content
- add usePageTitle hook that writes '<title> | Liqa' to document.title - list pages use section name; detail pages use entity name once loaded - create/edit pages use static action title, edit includes entity name
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const APP_NAME = 'Liqa';
|
||||
|
||||
/**
|
||||
* Sets document.title to "<title> | Liqa" for the current page.
|
||||
* Pass undefined or empty string to fall back to just "Liqa".
|
||||
*/
|
||||
export function usePageTitle(title: string | null | undefined): void {
|
||||
useEffect(() => {
|
||||
const prev = document.title;
|
||||
document.title = title ? `${title} | ${APP_NAME}` : APP_NAME;
|
||||
return () => {
|
||||
document.title = prev;
|
||||
};
|
||||
}, [title]);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, SubmitEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { KeyRound, X, Save } from 'lucide-react';
|
||||
import { credentials } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
|
||||
@@ -8,6 +9,7 @@ import styles from '../Page.module.css';
|
||||
|
||||
export function CreateCredentialPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('credentials.create_title'));
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Upload, Pencil, Trash2, KeyRound } from 'lucide-react';
|
||||
import { CodeBlock } from '../../ui';
|
||||
import { stringify as yamlStringify } from 'yaml';
|
||||
@@ -16,6 +17,7 @@ export function CredentialDetailPage() {
|
||||
const navigate = useNavigate();
|
||||
const [credential, setCredential] = useState<Credential | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
usePageTitle(credential?.name ?? null);
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Settings, Pencil, Trash2, Plus, Download, KeyRound } from 'lucide-react';
|
||||
import { parse as yamlParse } from 'yaml';
|
||||
import { credentials } from '../../api';
|
||||
@@ -100,6 +101,7 @@ function AddCredentialCard() {
|
||||
|
||||
export function CredentialsPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('credentials.title'));
|
||||
const navigate = useNavigate();
|
||||
const [items, setItems] = useState<Credential[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, SubmitEvent } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { KeyRound, X, Save } from 'lucide-react';
|
||||
import { credentials } from '../../api';
|
||||
import type { Credential } from '../../api';
|
||||
@@ -15,6 +16,7 @@ export function EditCredentialPage() {
|
||||
|
||||
const [credential, setCredential] = useState<Credential | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
usePageTitle(credential ? `${t('credentials.edit_title')} — ${credential.name}` : t('credentials.edit_title'));
|
||||
const [data, setData] = useState('');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const [dataError, setDataError] = useState('');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, SubmitEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { X, Globe, Save } from 'lucide-react';
|
||||
import { environments } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
|
||||
@@ -8,6 +9,7 @@ import styles from '../Page.module.css';
|
||||
|
||||
export function CreateEnvironmentPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('environments.create_title'));
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, SubmitEvent } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { X, Globe, Save } from 'lucide-react';
|
||||
import { environments } from '../../api';
|
||||
import type { Environment } from '../../api';
|
||||
@@ -15,6 +16,7 @@ export function EditEnvironmentPage() {
|
||||
|
||||
const [env, setEnv] = useState<Environment | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
usePageTitle(env ? `${t('environments.edit_title')} — ${env.name}` : t('environments.edit_title'));
|
||||
const [description, setDescription] = useState('');
|
||||
const [dataJson, setDataJson] = useState('{}');
|
||||
const [nameError, setNameError] = useState('');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Globe, Pencil, Trash2, Upload } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { stringify as yamlStringify } from 'yaml';
|
||||
import type { Environment } from '../../api';
|
||||
@@ -25,6 +26,7 @@ export function EnvironmentDetailPage() {
|
||||
const navigate = useNavigate();
|
||||
const [env, setEnv] = useState<Environment | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
usePageTitle(env?.name ?? null);
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const dataEntries = Object.entries(env?.data ?? {}).filter(([, v]) => v);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Settings, ExternalLink, Pencil, Trash2, Plus, Globe, Download } from 'lucide-react';
|
||||
import { parse as yamlParse } from 'yaml';
|
||||
import { stringify as yamlStringify } from 'yaml';
|
||||
@@ -117,6 +118,7 @@ function AddEnvironmentCard() {
|
||||
|
||||
export function EnvironmentsPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('environments.title'));
|
||||
const navigate = useNavigate();
|
||||
const [items, setItems] = useState<Environment[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Activity } from 'lucide-react';
|
||||
import { runs } from '../../api';
|
||||
import type { ScenarioRun, ScenarioRunStatus } from '../../api';
|
||||
@@ -38,6 +39,7 @@ type AllRunRow = ScenarioRun & {
|
||||
|
||||
export function AllRunsPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('runs.title'));
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [sortBy, setSortBy] = useState('createdAt');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { runs } from '../../api';
|
||||
import type {
|
||||
Scenario,
|
||||
@@ -69,6 +70,7 @@ export function RunDetailPage() {
|
||||
const [scenario, setScenario] = useState<Scenario | null>(null);
|
||||
const [run, setRun] = useState<ScenarioRunDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
usePageTitle(scenario ? `${t('runs.title')} — ${scenario.name}` : t('runs.title'));
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [polling, setPolling] = useState(false);
|
||||
const [pulseKey, setPulseKey] = useState(0);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Play, ClipboardList, Activity } from 'lucide-react';
|
||||
import { environments, scenarios, runs } from '../../api';
|
||||
import type {
|
||||
@@ -50,6 +51,7 @@ export function RunsPage() {
|
||||
|
||||
const [scenario, setScenario] = useState<Scenario | null>(null);
|
||||
const [items, setItems] = useState<RunRow[]>([]);
|
||||
usePageTitle(scenario ? `${t('runs.title')} — ${scenario.name}` : t('runs.title'));
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pulseKey, setPulseKey] = useState(0);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, SubmitEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { X, Save, ClipboardList } from 'lucide-react';
|
||||
import { scenarios, environments } from '../../api';
|
||||
import type { Environment } from '../../api';
|
||||
@@ -9,6 +10,7 @@ import styles from '../Page.module.css';
|
||||
|
||||
export function CreateScenarioPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('scenarios.create_title'));
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, SubmitEvent } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { X, Save, ClipboardList } from 'lucide-react';
|
||||
import { scenarios, steps } from '../../api';
|
||||
import type { Scenario } from '../../api';
|
||||
@@ -14,6 +15,7 @@ export function CreateStepPage() {
|
||||
const toast = useToast();
|
||||
|
||||
const [scenario, setScenario] = useState<Scenario | null>(null);
|
||||
usePageTitle(t('steps.create_title'));
|
||||
const [title, setTitle] = useState('');
|
||||
const [execCode, setExecCode] = useState('');
|
||||
const [timeoutSeconds, setTimeoutSeconds] = useState<string>('');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, SubmitEvent } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { X, Save, ClipboardList } from 'lucide-react';
|
||||
import { scenarios, environments } from '../../api';
|
||||
import type { Scenario, Environment } from '../../api';
|
||||
@@ -15,6 +16,7 @@ export function EditScenarioPage() {
|
||||
|
||||
const [scenario, setScenario] = useState<Scenario | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
usePageTitle(scenario ? `${t('scenarios.edit_title')} — ${scenario.name}` : t('scenarios.edit_title'));
|
||||
const [description, setDescription] = useState('');
|
||||
const [environmentId, setEnvironmentId] = useState<string>('');
|
||||
const [timeoutSeconds, setTimeoutSeconds] = useState<string>('');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, SubmitEvent } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { X, Save, ClipboardList } from 'lucide-react';
|
||||
import { scenarios, steps } from '../../api';
|
||||
import type { Scenario, ScenarioStep } from '../../api';
|
||||
@@ -15,6 +16,7 @@ export function EditStepPage() {
|
||||
|
||||
const [scenario, setScenario] = useState<Scenario | null>(null);
|
||||
const [step, setStep] = useState<ScenarioStep | null>(null);
|
||||
usePageTitle(step ? t('steps.edit_title', { order: step.order }) : t('steps.edit_title', { order: '' }));
|
||||
const [title, setTitle] = useState('');
|
||||
const [execCode, setExecCode] = useState('');
|
||||
const [timeoutSeconds, setTimeoutSeconds] = useState<string>('');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState, SubmitEvent } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import {
|
||||
Play,
|
||||
Pencil,
|
||||
@@ -51,6 +52,7 @@ export function ScenarioDetailPage() {
|
||||
const toast = useToast();
|
||||
const [scenario, setScenario] = useState<(Scenario & { steps: ScenarioStep[] }) | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
usePageTitle(scenario?.name ?? null);
|
||||
|
||||
// Credentials state
|
||||
const [scenarioCreds, setScenarioCreds] = useState<ScenarioCredential[]>([]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Play, Plus, Trash2, Download, ClipboardList } from 'lucide-react';
|
||||
import { parse as yamlParse } from 'yaml';
|
||||
import { environments, scenarios } from '../../api';
|
||||
@@ -38,6 +39,7 @@ const LAST_RUN_LABEL: Record<ScenarioRunStatus, string> = {
|
||||
|
||||
export function ScenariosPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('scenarios.title'));
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [items, setItems] = useState<Scenario[]>([]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Trash2, Monitor } from 'lucide-react';
|
||||
import { sessions } from '../../api';
|
||||
import type { Session } from '../../api';
|
||||
@@ -14,6 +15,7 @@ export function SessionDetailPage() {
|
||||
const navigate = useNavigate();
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
usePageTitle(session?.sessionName ?? null);
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Trash2, Monitor } from 'lucide-react';
|
||||
import { sessions } from '../../api';
|
||||
import type { Session } from '../../api';
|
||||
@@ -19,6 +20,7 @@ import styles from '../Page.module.css';
|
||||
|
||||
export function SessionsPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('sessions.title'));
|
||||
const navigate = useNavigate();
|
||||
const [items, setItems] = useState<Session[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, SubmitEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { X, Save, Braces } from 'lucide-react';
|
||||
import { snippets } from '../../api';
|
||||
import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
|
||||
@@ -8,6 +9,7 @@ import styles from '../Page.module.css';
|
||||
|
||||
export function CreateSnippetPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('snippets.create_title'));
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, SubmitEvent } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { X, Save, Braces } from 'lucide-react';
|
||||
import { snippets } from '../../api';
|
||||
import type { Snippet } from '../../api';
|
||||
@@ -15,6 +16,7 @@ export function EditSnippetPage() {
|
||||
|
||||
const [snippet, setSnippet] = useState<Snippet | null>(null);
|
||||
const [alias, setAlias] = useState('');
|
||||
usePageTitle(snippet ? `${t('snippets.edit_title')} — ${snippet.title}` : t('snippets.edit_title'));
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Upload, Pencil, Trash2, Braces } from 'lucide-react';
|
||||
import { CodeBlock } from '../../ui';
|
||||
import { stringify as yamlStringify } from 'yaml';
|
||||
@@ -24,6 +25,7 @@ export function SnippetDetailPage() {
|
||||
const navigate = useNavigate();
|
||||
const [snippet, setSnippet] = useState<Snippet | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
usePageTitle(snippet?.title ?? null);
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePageTitle } from '../../hooks/usePageTitle';
|
||||
import { Settings, Pencil, Trash2, Plus, Download, Braces } from 'lucide-react';
|
||||
import { parse as yamlParse } from 'yaml';
|
||||
import { snippets } from '../../api';
|
||||
@@ -114,6 +115,7 @@ function AddSnippetCard() {
|
||||
|
||||
export function SnippetsPage() {
|
||||
const { t } = useTranslation();
|
||||
usePageTitle(t('snippets.title'));
|
||||
const navigate = useNavigate();
|
||||
const [items, setItems] = useState<Snippet[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
Reference in New Issue
Block a user