mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-09 15:05:26 +03:00
Compare commits
22 Commits
0246aa1e6f
...
ef0e21b3bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef0e21b3bf | ||
|
|
a87c1e850e | ||
|
|
ca484cc895 | ||
|
|
7117d01b6d | ||
|
|
488235619b | ||
|
|
e86ef43e44 | ||
|
|
018c6a6b10 | ||
|
|
0b555cb934 | ||
|
|
81cb14cf91 | ||
|
|
a8f779496e | ||
|
|
b501ba131c | ||
|
|
6936abba80 | ||
|
|
eb73e1e7e0 | ||
|
|
f1c70eff0b | ||
|
|
dba3b93d36 | ||
|
|
200184c321 | ||
|
|
e58c73552e | ||
|
|
579175b9e7 | ||
|
|
9d280788df | ||
|
|
5ab6e47303 | ||
|
|
ea246384bf | ||
|
|
d8f70b7fed |
@ -8,7 +8,9 @@ class ReviewMetadata(BaseModel):
|
|||||||
description="A short title characterizing what took place and where, under 10 words."
|
description="A short title characterizing what took place and where, under 10 words."
|
||||||
)
|
)
|
||||||
scene: str = Field(
|
scene: str = Field(
|
||||||
description="A chronological narrative of what happens from start to finish."
|
min_length=120,
|
||||||
|
max_length=600,
|
||||||
|
description="A chronological narrative of what happens from start to finish.",
|
||||||
)
|
)
|
||||||
shortSummary: str = Field(
|
shortSummary: str = Field(
|
||||||
description="A brief 2-sentence summary of the scene, suitable for notifications."
|
description="A brief 2-sentence summary of the scene, suitable for notifications."
|
||||||
|
|||||||
@ -358,6 +358,156 @@ test.describe("FaceSelectionDialog @high", () => {
|
|||||||
await frigateApp.page.keyboard.press("Escape");
|
await frigateApp.page.keyboard.press("Escape");
|
||||||
await expect(menu).not.toBeVisible({ timeout: 3_000 });
|
await expect(menu).not.toBeVisible({ timeout: 3_000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("classifying the last image in a group leaves body interactive", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
|
// Regression guard for the stuck body pointer-events bug when the
|
||||||
|
// last image in a grouped-recognition detail Dialog is classified.
|
||||||
|
// Tracked upstream at radix-ui/primitives#3445.
|
||||||
|
//
|
||||||
|
// Root cause: when the user clicks a FaceSelectionDialog menu item,
|
||||||
|
// the modal DropdownMenu enters its exit animation (Radix's Presence
|
||||||
|
// keeps it in the DOM with data-state="closed" until animationend).
|
||||||
|
// While that is in flight the classify axios resolves, SWR removes
|
||||||
|
// the image from /api/faces, the parent's map no longer renders the
|
||||||
|
// grouped card, and React unmounts the subtree — including the still-
|
||||||
|
// animating DropdownMenu's Presence container. DismissableLayer's
|
||||||
|
// shared modal-layer stack can't reconcile the interrupted exit, so
|
||||||
|
// the `body { pointer-events: none }` entry it put on mount is never
|
||||||
|
// popped and the rest of the UI becomes unclickable.
|
||||||
|
//
|
||||||
|
// The fix is `modal={false}` on the FaceSelectionDialog's
|
||||||
|
// DropdownMenu (desktop path only). With modal=false the DropdownMenu
|
||||||
|
// never puts an entry on DismissableLayer's body-pointer-events stack
|
||||||
|
// in the first place, so there's nothing to leak when its Presence is
|
||||||
|
// torn down mid-animation. The Radix-community-documented workaround
|
||||||
|
// for #3445.
|
||||||
|
//
|
||||||
|
// The bug only reproduces when the mock resolves fast enough that
|
||||||
|
// the parent unmounts before the dropdown's exit animation finishes.
|
||||||
|
// Measured window via a 3x sweep on the pre-fix build: 0–200 ms
|
||||||
|
// triggers it; 300 ms+ no longer reproduces. Production LAN networks
|
||||||
|
// sit comfortably inside the bad window, while `npm run dev` seems
|
||||||
|
// to mask it via React StrictMode's double-effect scheduling.
|
||||||
|
const EVENT_ID = "1775487131.3863528-race";
|
||||||
|
const initialFaces = withGroupedTrainingAttempt(basicFacesMock(), {
|
||||||
|
eventId: EVENT_ID,
|
||||||
|
attempts: [
|
||||||
|
{ timestamp: 1775487131.3863528, label: "unknown", score: 0.95 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let classified = false;
|
||||||
|
|
||||||
|
await frigateApp.installDefaults({
|
||||||
|
faces: initialFaces,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
id: EVENT_ID,
|
||||||
|
label: "person",
|
||||||
|
sub_label: null,
|
||||||
|
camera: "front_door",
|
||||||
|
start_time: 1775487131.3863528,
|
||||||
|
end_time: 1775487161.3863528,
|
||||||
|
false_positive: false,
|
||||||
|
zones: ["front_yard"],
|
||||||
|
thumbnail: null,
|
||||||
|
has_clip: true,
|
||||||
|
has_snapshot: true,
|
||||||
|
retain_indefinitely: false,
|
||||||
|
plus_id: null,
|
||||||
|
model_hash: "abc123",
|
||||||
|
detector_type: "cpu",
|
||||||
|
model_type: "ssd",
|
||||||
|
data: {
|
||||||
|
top_score: 0.92,
|
||||||
|
score: 0.92,
|
||||||
|
region: [0.1, 0.1, 0.5, 0.8],
|
||||||
|
box: [0.2, 0.15, 0.45, 0.75],
|
||||||
|
area: 0.18,
|
||||||
|
ratio: 0.6,
|
||||||
|
type: "object",
|
||||||
|
path_data: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-route /api/faces to flip to the "train empty" payload once the
|
||||||
|
// classify POST has been received. Registered AFTER installDefaults so
|
||||||
|
// Playwright's LIFO route matching hits this handler first.
|
||||||
|
await frigateApp.page.route("**/api/faces", async (route) => {
|
||||||
|
const payload = classified ? basicFacesMock() : initialFaces;
|
||||||
|
await route.fulfill({ json: payload });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hold the classify POST briefly. The race opens when the parent
|
||||||
|
// unmounts before the dropdown's exit animation finishes (~200ms
|
||||||
|
// in Radix). 100ms keeps us comfortably inside that window and
|
||||||
|
// reliably triggered the bug in a 3x sweep across 0/50/100/200ms
|
||||||
|
// on the pre-fix build. CLASSIFY_DELAY_MS overrides for local sweeps.
|
||||||
|
const delayMs = Number(
|
||||||
|
(globalThis as { process?: { env?: Record<string, string> } }).process
|
||||||
|
?.env?.CLASSIFY_DELAY_MS ?? "100",
|
||||||
|
);
|
||||||
|
await frigateApp.page.route(
|
||||||
|
"**/api/faces/train/*/classify",
|
||||||
|
async (route) => {
|
||||||
|
classified = true;
|
||||||
|
if (delayMs > 0) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||||
|
}
|
||||||
|
await route.fulfill({ json: { success: true } });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await frigateApp.goto("/faces");
|
||||||
|
|
||||||
|
// Open the grouped detail Dialog.
|
||||||
|
const groupedImage = frigateApp.page
|
||||||
|
.locator('img[src*="clips/faces/train/"]')
|
||||||
|
.first();
|
||||||
|
await expect(groupedImage).toBeVisible({ timeout: 5_000 });
|
||||||
|
await groupedImage.locator("xpath=..").click();
|
||||||
|
const dialog = frigateApp.page
|
||||||
|
.getByRole("dialog")
|
||||||
|
.filter({ has: frigateApp.page.locator('img[src*="clips/faces/train/"]') })
|
||||||
|
.first();
|
||||||
|
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
||||||
|
|
||||||
|
// Single attempt → single `+` trigger.
|
||||||
|
const triggers = dialog.locator('[aria-haspopup="menu"]');
|
||||||
|
await expect(triggers).toHaveCount(1);
|
||||||
|
await triggers.first().click();
|
||||||
|
|
||||||
|
const menu = frigateApp.page
|
||||||
|
.locator('[role="menu"], [data-radix-menu-content]')
|
||||||
|
.first();
|
||||||
|
await expect(menu).toBeVisible({ timeout: 5_000 });
|
||||||
|
await menu.getByRole("menuitem", { name: /^alice$/i }).click();
|
||||||
|
|
||||||
|
// The Dialog must leave the tree cleanly, and body must recover.
|
||||||
|
await expect(dialog).not.toBeVisible({ timeout: 5_000 });
|
||||||
|
|
||||||
|
// Give Radix's exit animation + cleanup a comfortable margin on top of
|
||||||
|
// the ~300ms simulated network delay.
|
||||||
|
await waitForBodyInteractive(frigateApp.page, 5_000);
|
||||||
|
await expectBodyInteractive(frigateApp.page);
|
||||||
|
|
||||||
|
// User-visible confirmation: click something outside the dialog
|
||||||
|
// and assert it actually responds.
|
||||||
|
const librarySelector = frigateApp.page
|
||||||
|
.getByRole("button")
|
||||||
|
.filter({ hasText: /\(\d+\)/ })
|
||||||
|
.first();
|
||||||
|
await librarySelector.click();
|
||||||
|
await expect(
|
||||||
|
frigateApp.page
|
||||||
|
.locator('[role="menu"], [data-radix-menu-content]')
|
||||||
|
.first(),
|
||||||
|
).toBeVisible({ timeout: 3_000 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Face Library — mobile @high @mobile", () => {
|
test.describe("Face Library — mobile @high @mobile", () => {
|
||||||
|
|||||||
@ -1281,7 +1281,7 @@
|
|||||||
"hikvision": {
|
"hikvision": {
|
||||||
"substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Hikvision suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles."
|
"substreamWarning": "El substream 1 està bloquejat a una resolució baixa. Moltes càmeres Hikvision suporten subfluxos addicionals que han d'estar habilitats a la configuració de la càmera. Es recomana comprovar i utilitzar aquests corrents si estan disponibles."
|
||||||
},
|
},
|
||||||
"resolutionUnknown": "La resolució d'aquest flux no s'ha pogut investigar. Això causarà problemes a l'inici. Heu d'establir manualment la resolució de detecció a Configuració o a la configuració."
|
"resolutionUnknown": "La resolució d'aquest flux no s'ha pogut investigar. Heu d'establir manualment la resolució de detecció a Configuració o a la configuració."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -279,7 +279,7 @@
|
|||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"next": {
|
"next": {
|
||||||
"title": "Successiva",
|
"title": "Successivo",
|
||||||
"label": "Vai alla pagina successiva"
|
"label": "Vai alla pagina successiva"
|
||||||
},
|
},
|
||||||
"previous": {
|
"previous": {
|
||||||
|
|||||||
@ -14,7 +14,9 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"error": {
|
"error": {
|
||||||
"renameExportFailed": "Impossibile rinominare l'esportazione: {{errorMessage}}",
|
"renameExportFailed": "Impossibile rinominare l'esportazione: {{errorMessage}}",
|
||||||
"assignCaseFailed": "Impossibile aggiornare l'assegnazione del caso: {{errorMessage}}"
|
"assignCaseFailed": "Impossibile aggiornare l'assegnazione del caso: {{errorMessage}}",
|
||||||
|
"caseSaveFailed": "Impossibile salvare il caso: {{errorMessage}}",
|
||||||
|
"caseDeleteFailed": "Impossibile eliminare il caso: {{errorMessage}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
@ -22,7 +24,8 @@
|
|||||||
"downloadVideo": "Scarica video",
|
"downloadVideo": "Scarica video",
|
||||||
"editName": "Modifica nome",
|
"editName": "Modifica nome",
|
||||||
"deleteExport": "Elimina esportazione",
|
"deleteExport": "Elimina esportazione",
|
||||||
"assignToCase": "Aggiungi al caso"
|
"assignToCase": "Aggiungi al caso",
|
||||||
|
"removeFromCase": "Rimuovi dal caso"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"cases": "Casi",
|
"cases": "Casi",
|
||||||
@ -35,5 +38,91 @@
|
|||||||
"newCaseOption": "Crea un nuovo caso",
|
"newCaseOption": "Crea un nuovo caso",
|
||||||
"nameLabel": "Nome del caso",
|
"nameLabel": "Nome del caso",
|
||||||
"descriptionLabel": "Descrizione"
|
"descriptionLabel": "Descrizione"
|
||||||
|
},
|
||||||
|
"toolbar": {
|
||||||
|
"newCase": "Nuovo caso",
|
||||||
|
"addExport": "Aggiungi esportazione",
|
||||||
|
"editCase": "Modifica caso",
|
||||||
|
"deleteCase": "Elimina il caso"
|
||||||
|
},
|
||||||
|
"deleteCase": {
|
||||||
|
"label": "Eliminare il caso",
|
||||||
|
"desc": "Sei sicuro di voler eliminare {{caseName}}?",
|
||||||
|
"descKeepExports": "Le esportazioni rimarranno disponibili come esportazioni non categorizzate.",
|
||||||
|
"descDeleteExports": "In questo caso, tutte le esportazioni verranno eliminate definitivamente.",
|
||||||
|
"deleteExports": "Elimina anche le esportazioni"
|
||||||
|
},
|
||||||
|
"caseCard": {
|
||||||
|
"emptyCase": "Ancora nessuna esportazione"
|
||||||
|
},
|
||||||
|
"jobCard": {
|
||||||
|
"defaultName": "{{camera}} esporta",
|
||||||
|
"queued": "In coda",
|
||||||
|
"running": "In esecuzione",
|
||||||
|
"preparing": "Preparazione",
|
||||||
|
"copying": "Copia",
|
||||||
|
"encoding": "Codifica",
|
||||||
|
"encodingRetry": "Codifica (riprova)",
|
||||||
|
"finalizing": "Finalizzazione"
|
||||||
|
},
|
||||||
|
"caseView": {
|
||||||
|
"noDescription": "Nessuna descrizione",
|
||||||
|
"createdAt": "Creato {{value}}",
|
||||||
|
"exportCount_one": "1 esportazione",
|
||||||
|
"exportCount_other": "{{count}} esportazioni",
|
||||||
|
"cameraCount_one": "1 telecamera",
|
||||||
|
"cameraCount_other": "{{count}} telecamere",
|
||||||
|
"showMore": "Mostra altro",
|
||||||
|
"showLess": "Mostra meno",
|
||||||
|
"emptyTitle": "Questo caso è vuoto",
|
||||||
|
"emptyDescription": "Aggiungi le esportazioni esistenti non categorizzate per mantenere il caso organizzato.",
|
||||||
|
"emptyDescriptionNoExports": "Al momento non sono disponibili esportazioni non categorizzate da aggiungere."
|
||||||
|
},
|
||||||
|
"caseEditor": {
|
||||||
|
"createTitle": "Crea un caso",
|
||||||
|
"editTitle": "Modifica caso",
|
||||||
|
"namePlaceholder": "Nome del caso",
|
||||||
|
"descriptionPlaceholder": "Aggiungi note o contesto per questo caso"
|
||||||
|
},
|
||||||
|
"addExportDialog": {
|
||||||
|
"title": "Aggiungi l'esportazione a {{caseName}}",
|
||||||
|
"searchPlaceholder": "Cerca esportazioni non categorizzate",
|
||||||
|
"empty": "Nessuna esportazione non categorizzata corrisponde a questa ricerca.",
|
||||||
|
"addButton_one": "Aggiungi 1 esportazione",
|
||||||
|
"addButton_other": "Aggiungi {{count}} esportazioni",
|
||||||
|
"adding": "In aggiunta..."
|
||||||
|
},
|
||||||
|
"selected_one": "{{count}} selezionati",
|
||||||
|
"selected_other": "{{count}} selezionati",
|
||||||
|
"bulkActions": {
|
||||||
|
"addToCase": "Aggiungi al caso",
|
||||||
|
"moveToCase": "Sposta al caso",
|
||||||
|
"removeFromCase": "Rimuovi dal caso",
|
||||||
|
"delete": "Elimina",
|
||||||
|
"deleteNow": "Elimina ora"
|
||||||
|
},
|
||||||
|
"bulkDelete": {
|
||||||
|
"title": "Elimina le esportazioni",
|
||||||
|
"desc_one": "Sei sicuro di voler eliminare l'esportazione {{count}}?",
|
||||||
|
"desc_other": "Sei sicuro di voler eliminare {{count}} esportazioni?"
|
||||||
|
},
|
||||||
|
"bulkRemoveFromCase": {
|
||||||
|
"title": "Rimuovi dal caso",
|
||||||
|
"desc_one": "Rimuovere l'esportazione {{count}} da questo caso?",
|
||||||
|
"desc_other": "Rimuovere {{count}} esportazioni da questo caso?",
|
||||||
|
"descKeepExports": "Le esportazioni verranno spostate nella categoria \"non classificate\".",
|
||||||
|
"descDeleteExports": "Le esportazioni verranno eliminate definitivamente.",
|
||||||
|
"deleteExports": "Elimina almeno le esportazioni"
|
||||||
|
},
|
||||||
|
"bulkToast": {
|
||||||
|
"success": {
|
||||||
|
"delete": "Esportazioni eliminate con successo",
|
||||||
|
"reassign": "Assegnazione del caso aggiornata con successo",
|
||||||
|
"remove": "Esportazioni rimosse con successo dal caso"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"deleteFailed": "Impossibile eliminare le esportazioni: {{errorMessage}}",
|
||||||
|
"reassignFailed": "Impossibile aggiornare l'assegnazione del caso: {{errorMessage}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -268,7 +268,7 @@
|
|||||||
"description": "为此摄像头启用和控制通知的设置。"
|
"description": "为此摄像头启用和控制通知的设置。"
|
||||||
},
|
},
|
||||||
"live": {
|
"live": {
|
||||||
"label": "实时回放",
|
"label": "实时监控观看",
|
||||||
"streams": {
|
"streams": {
|
||||||
"label": "实时监控流名称",
|
"label": "实时监控流名称",
|
||||||
"description": "配置的流名称到用于实时监控播放的 restream/go2rtc 名称的映射。"
|
"description": "配置的流名称到用于实时监控播放的 restream/go2rtc 名称的映射。"
|
||||||
|
|||||||
@ -1486,7 +1486,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"live": {
|
"live": {
|
||||||
"label": "实时回放",
|
"label": "实时监控观看",
|
||||||
"description": "用于控制 JSMPEG 实时流分辨率与画质的设置。此设置不影响使用 go2rtc 进行实时预览的摄像头。",
|
"description": "用于控制 JSMPEG 实时流分辨率与画质的设置。此设置不影响使用 go2rtc 进行实时预览的摄像头。",
|
||||||
"streams": {
|
"streams": {
|
||||||
"label": "实时监控流名称",
|
"label": "实时监控流名称",
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
"globalObjects": "目标",
|
"globalObjects": "目标",
|
||||||
"globalReview": "核查",
|
"globalReview": "核查",
|
||||||
"globalAudioEvents": "音频事件",
|
"globalAudioEvents": "音频事件",
|
||||||
"globalLivePlayback": "实时回放",
|
"globalLivePlayback": "实时监控观看",
|
||||||
"globalTimestampStyle": "时间戳样式",
|
"globalTimestampStyle": "时间戳样式",
|
||||||
"systemDatabase": "数据库",
|
"systemDatabase": "数据库",
|
||||||
"systemTls": "TLS加密链接",
|
"systemTls": "TLS加密链接",
|
||||||
@ -78,7 +78,7 @@
|
|||||||
"cameraAudioEvents": "音频事件",
|
"cameraAudioEvents": "音频事件",
|
||||||
"cameraAudioTranscription": "音频转录",
|
"cameraAudioTranscription": "音频转录",
|
||||||
"cameraNotifications": "通知",
|
"cameraNotifications": "通知",
|
||||||
"cameraLivePlayback": "实时回放",
|
"cameraLivePlayback": "实时监控观看",
|
||||||
"cameraBirdseye": "鸟瞰图",
|
"cameraBirdseye": "鸟瞰图",
|
||||||
"cameraFaceRecognition": "人脸识别",
|
"cameraFaceRecognition": "人脸识别",
|
||||||
"cameraLpr": "车牌识别",
|
"cameraLpr": "车牌识别",
|
||||||
|
|||||||
@ -266,7 +266,7 @@ export function ExportCard({
|
|||||||
)}
|
)}
|
||||||
{!exportedRecording.in_progress && !selectionMode && (
|
{!exportedRecording.in_progress && !selectionMode && (
|
||||||
<div className="absolute bottom-2 right-3 z-40">
|
<div className="absolute bottom-2 right-3 z-40">
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<BlurredIconButton
|
<BlurredIconButton
|
||||||
aria-label={t("tooltip.editName")}
|
aria-label={t("tooltip.editName")}
|
||||||
|
|||||||
@ -275,7 +275,7 @@ export default function ReviewCard({
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
<ContextMenu key={event.id}>
|
<ContextMenu key={event.id} modal={false}>
|
||||||
<ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
|
<ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem>
|
<ContextMenuItem>
|
||||||
|
|||||||
@ -272,7 +272,7 @@ export default function LiveContextMenu({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full", className)}>
|
<div className={cn("w-full", className)}>
|
||||||
<ContextMenu key={camera} onOpenChange={handleOpenChange}>
|
<ContextMenu key={camera} modal={false} onOpenChange={handleOpenChange}>
|
||||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<div className="flex flex-col items-start gap-1 py-1 pl-2">
|
<div className="flex flex-col items-start gap-1 py-1 pl-2">
|
||||||
|
|||||||
@ -258,13 +258,13 @@ export default function SearchResultActions({
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
{isContextMenu ? (
|
{isContextMenu ? (
|
||||||
<ContextMenu>
|
<ContextMenu modal={false}>
|
||||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||||
<ContextMenuContent>{menuItems}</ContextMenuContent>
|
<ContextMenuContent>{menuItems}</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<BlurredIconButton aria-label={t("itemMenu.more.aria")}>
|
<BlurredIconButton aria-label={t("itemMenu.more.aria")}>
|
||||||
<FiMoreVertical className="size-5" />
|
<FiMoreVertical className="size-5" />
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export default function ActionsDropdown({
|
|||||||
const { t } = useTranslation(["components/dialog", "views/replay", "common"]);
|
const { t } = useTranslation(["components/dialog", "views/replay", "common"]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
|
|||||||
@ -124,7 +124,7 @@ export default function ClassificationSelectionDialog({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Selector>
|
<Selector {...(isDesktop ? { modal: false } : {})}>
|
||||||
<SelectorTrigger asChild>
|
<SelectorTrigger asChild>
|
||||||
<TooltipTrigger asChild={isChildButton}>{children}</TooltipTrigger>
|
<TooltipTrigger asChild={isChildButton}>{children}</TooltipTrigger>
|
||||||
</SelectorTrigger>
|
</SelectorTrigger>
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export default function FaceSelectionDialog({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Selector>
|
<Selector {...(isDesktop ? { modal: false } : {})}>
|
||||||
<SelectorTrigger asChild>
|
<SelectorTrigger asChild>
|
||||||
<TooltipTrigger asChild={isChildButton}>{children}</TooltipTrigger>
|
<TooltipTrigger asChild={isChildButton}>{children}</TooltipTrigger>
|
||||||
</SelectorTrigger>
|
</SelectorTrigger>
|
||||||
|
|||||||
@ -594,7 +594,7 @@ function LibrarySelector({
|
|||||||
forbiddenErrorMessage={t("description.nameCannotContainHash")}
|
forbiddenErrorMessage={t("description.nameCannotContainHash")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="flex justify-between smart-capitalize">
|
<Button className="flex justify-between smart-capitalize">
|
||||||
{pageTitle}
|
{pageTitle}
|
||||||
|
|||||||
@ -342,7 +342,7 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
|
|||||||
{config.name}
|
{config.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-2 right-2 z-40">
|
<div className="absolute bottom-2 right-2 z-40">
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||||
<BlurredIconButton>
|
<BlurredIconButton>
|
||||||
<FiMoreVertical className="size-5 text-white" />
|
<FiMoreVertical className="size-5 text-white" />
|
||||||
|
|||||||
@ -698,7 +698,7 @@ function LibrarySelector({
|
|||||||
regexErrorMessage={t("description.invalidName")}
|
regexErrorMessage={t("description.invalidName")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="flex justify-between smart-capitalize">
|
<Button className="flex justify-between smart-capitalize">
|
||||||
{pageTitle}
|
{pageTitle}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user