From 97d63b0269877a8eea21bf6659bf266ac02bd3a9 Mon Sep 17 00:00:00 2001 From: Muaz Date: Wed, 24 Jun 2026 23:51:18 -0500 Subject: [PATCH 1/5] Add context menu to insert menu --- src/frontend/app-common/app-menu.tsx | 25 +++++++++++++++-- src/frontend/cards/card-components.tsx | 31 ++++----------------- src/frontend/cards/insertable-card.tsx | 24 +++++++++++----- src/frontend/insert/insert-menu.tsx | 10 +++++++ src/frontend/routes/app/groups/$groupId.tsx | 4 +-- 5 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/frontend/app-common/app-menu.tsx b/src/frontend/app-common/app-menu.tsx index b417ff82..c634db1f 100644 --- a/src/frontend/app-common/app-menu.tsx +++ b/src/frontend/app-common/app-menu.tsx @@ -1,5 +1,7 @@ -import { ReactNode } from "react"; -import { FloatingPosition, Menu } from "@mantine/core"; +import { PropsWithChildren, ReactNode } from "react"; +import { FloatingPosition, Menu, ActionIcon } from "@mantine/core"; +import { IconDots } from "@tabler/icons-react"; +import { IconSize } from "../common/style-constants"; interface AppContextMenuProps { menuItems: ReactNode; @@ -56,3 +58,22 @@ export function AppContextMenu(props: AppContextMenuProps): ReactNode { ); } + +/** + * An explicit button which opens a menu with the given items. Used alongside + * the right-click context menu so the menu is reachable without a right-click. + */ +export function MenuButton(props: PropsWithChildren): ReactNode { + return ( + + e.stopPropagation()} + > + + + + ); +} diff --git a/src/frontend/cards/card-components.tsx b/src/frontend/cards/card-components.tsx index 75a0fdd4..4a6981ea 100644 --- a/src/frontend/cards/card-components.tsx +++ b/src/frontend/cards/card-components.tsx @@ -1,6 +1,5 @@ -import { ActionIcon, Badge, Group, Menu, Table, Text } from "@mantine/core"; +import { Badge, Group, Menu, Table, Text } from "@mantine/core"; import { - IconDots, IconExternalLink, IconEyeOff, IconLink, @@ -11,11 +10,11 @@ import { import { IconSize } from "../common/style-constants"; import { copyUrlToClipboard, makeUrl, openUrlInNewTab } from "../common/url"; import { PropsWithChildren, ReactNode, useCallback } from "react"; -import { AppContextMenu } from "../app-common/app-menu"; +import { AppContextMenu, MenuButton } from "../app-common/app-menu"; import { SearchHit } from "../search/search"; import { SearchHitTitle } from "../search/search-results"; import { CardThumbnail } from "../insert/thumbnail"; -import { DocumentPath } from "../../shared/onshape-path"; +import { ElementPath } from "../../shared/onshape-path"; import { openCannotDeriveAssemblyAlert } from "../app/alerts"; import { useInsertMutation, @@ -30,14 +29,15 @@ import { RequireAccessLevel } from "../api-utils/access-level"; import { useReloadThumbnailMutation } from "./card-hooks"; interface OpenDocumentItemsProps { - path: DocumentPath; + path: ElementPath; + configuration?: Configuration; } /** * Menu items which can be used to open or copy a link to a document. */ export function OpenDocumentItems(props: OpenDocumentItemsProps) { - const url = makeUrl(props.path); + const url = makeUrl(props.path, props.configuration); return ( <> - e.stopPropagation()} - > - - - - ); -} - /** * Wraps one or more admin-only menu items into an Admin submenu. */ diff --git a/src/frontend/cards/insertable-card.tsx b/src/frontend/cards/insertable-card.tsx index f28c227c..b83f5517 100644 --- a/src/frontend/cards/insertable-card.tsx +++ b/src/frontend/cards/insertable-card.tsx @@ -15,6 +15,7 @@ import { InsertableOut, LibraryOut } from "../../shared/api-models"; +import { Configuration } from "../../shared/configuration-models"; import { ElementType } from "../../shared/types"; import { SearchHit } from "../search/search"; import { @@ -112,26 +113,35 @@ export function InsertableCard(props: InsertableCardProps): ReactNode { interface InsertableMenuItemsProps { favorite: Favorite | undefined; insertable: InsertableOut; + inInsertMenu?: boolean; + configuration?: Configuration; } export function InsertableMenuItems( props: InsertableMenuItemsProps ): ReactNode { - const { favorite, insertable } = props; + const { favorite, insertable, inInsertMenu, configuration } = props; return ( <> - - + {!inInsertMenu && ( + <> + + + + )} - + diff --git a/src/frontend/insert/insert-menu.tsx b/src/frontend/insert/insert-menu.tsx index 8b10bbc3..58254ab3 100644 --- a/src/frontend/insert/insert-menu.tsx +++ b/src/frontend/insert/insert-menu.tsx @@ -16,6 +16,8 @@ import { NotificationAction, renderNotification } from "../common/notifications"; +import { MenuButton } from "../app-common/app-menu"; +import { InsertableMenuItems } from "../cards/insertable-card"; import { ConfigurationWrapper } from "./configurations"; import { useInsertMutation } from "./insert-hooks"; import { Configuration } from "../../shared/configuration-models"; @@ -94,6 +96,14 @@ function InsertMenuContent(props: InsertMenuContentProps): ReactNode { {parameters} + + + Date: Thu, 25 Jun 2026 15:05:08 -0500 Subject: [PATCH 2/5] Fix makeUrl overload for DocumentPath with configuration --- src/frontend/cards/card-components.tsx | 4 ++-- src/frontend/common/url.tsx | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/frontend/cards/card-components.tsx b/src/frontend/cards/card-components.tsx index 4a6981ea..5eeb07ac 100644 --- a/src/frontend/cards/card-components.tsx +++ b/src/frontend/cards/card-components.tsx @@ -14,7 +14,7 @@ import { AppContextMenu, MenuButton } from "../app-common/app-menu"; import { SearchHit } from "../search/search"; import { SearchHitTitle } from "../search/search-results"; import { CardThumbnail } from "../insert/thumbnail"; -import { ElementPath } from "../../shared/onshape-path"; +import { DocumentPath } from "../../shared/onshape-path"; import { openCannotDeriveAssemblyAlert } from "../app/alerts"; import { useInsertMutation, @@ -29,7 +29,7 @@ import { RequireAccessLevel } from "../api-utils/access-level"; import { useReloadThumbnailMutation } from "./card-hooks"; interface OpenDocumentItemsProps { - path: ElementPath; + path: DocumentPath; configuration?: Configuration; } diff --git a/src/frontend/common/url.tsx b/src/frontend/common/url.tsx index d93e6834..c931eca0 100644 --- a/src/frontend/common/url.tsx +++ b/src/frontend/common/url.tsx @@ -11,6 +11,10 @@ import { notifications } from "@mantine/notifications"; import { IconLink } from "@tabler/icons-react"; import { IconSize } from "./style-constants"; +export function makeUrl( + path: DocumentPath, + configuration?: Record +): string; export function makeUrl(path: DocumentPath): string; export function makeUrl(path: InstancePath): string; export function makeUrl(path: ElementPath): string; From b294c5d8fcea8ca41abb6c609b8566d521187859 Mon Sep 17 00:00:00 2001 From: Muaz Date: Wed, 1 Jul 2026 23:13:59 -0500 Subject: [PATCH 3/5] Changed to use an instance path by adding a configurable path --- src/frontend/cards/card-components.tsx | 9 +++++---- src/frontend/common/url.tsx | 25 +++++++++++-------------- src/shared/onshape-path.ts | 13 +++++++++++++ 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/frontend/cards/card-components.tsx b/src/frontend/cards/card-components.tsx index 5eeb07ac..136a1bc0 100644 --- a/src/frontend/cards/card-components.tsx +++ b/src/frontend/cards/card-components.tsx @@ -14,7 +14,7 @@ import { AppContextMenu, MenuButton } from "../app-common/app-menu"; import { SearchHit } from "../search/search"; import { SearchHitTitle } from "../search/search-results"; import { CardThumbnail } from "../insert/thumbnail"; -import { DocumentPath } from "../../shared/onshape-path"; +import { InstancePath, isElementPath } from "../../shared/onshape-path"; import { openCannotDeriveAssemblyAlert } from "../app/alerts"; import { useInsertMutation, @@ -29,15 +29,16 @@ import { RequireAccessLevel } from "../api-utils/access-level"; import { useReloadThumbnailMutation } from "./card-hooks"; interface OpenDocumentItemsProps { - path: DocumentPath; + path: InstancePath; configuration?: Configuration; } - /** * Menu items which can be used to open or copy a link to a document. */ export function OpenDocumentItems(props: OpenDocumentItemsProps) { - const url = makeUrl(props.path, props.configuration); + const url = isElementPath(props.path) + ? makeUrl({ ...props.path, configuration: props.configuration }) + : makeUrl(props.path); return ( <> -): string; -export function makeUrl(path: DocumentPath): string; -export function makeUrl(path: InstancePath): string; +export function makeUrl(path: ConfigurablePath): string; export function makeUrl(path: ElementPath): string; -export function makeUrl( - path: ElementPath, - configuration?: Record -): string; +export function makeUrl(path: InstancePath): string; +export function makeUrl(path: DocumentPath): string; export function makeUrl( path: DocumentPath, configuration?: Record @@ -33,12 +28,14 @@ export function makeUrl( if (isElementPath(path)) { url += `/e/${path.elementId}`; } - if (configuration) { - url += "?configuration=" + encodeConfigurationForQuery(configuration); + const config = + configuration ?? + (isConfigurablePath(path) ? path.configuration : undefined); + if (config) { + url += "?configuration=" + encodeConfigurationForQuery(config); } return url; } - export interface ConfigurationPath { configuration?: string; } diff --git a/src/shared/onshape-path.ts b/src/shared/onshape-path.ts index ea7fd3ed..46985d2c 100644 --- a/src/shared/onshape-path.ts +++ b/src/shared/onshape-path.ts @@ -18,6 +18,10 @@ export interface PartPath extends ElementPath { partId: string; } +export interface ConfigurablePath extends ElementPath { + configuration?: Record; +} + export function isDocumentPath(path: any): path is DocumentPath { return ( typeof path === "object" && @@ -44,6 +48,15 @@ export function isPartPath(path: DocumentPath): path is PartPath { return isElementPath(path) && (path as PartPath).partId !== undefined; } +export function isConfigurablePath( + path: DocumentPath +): path is ConfigurablePath { + return ( + isElementPath(path) && + (path as ConfigurablePath).configuration !== undefined + ); +} + export function toDocumentApiPath(path: DocumentPath): string { return `/d/${path.documentId}`; } From f7ec8a54cd12e99221f5f7ce43291797c6c4dc30 Mon Sep 17 00:00:00 2001 From: Muaz Date: Wed, 1 Jul 2026 23:18:32 -0500 Subject: [PATCH 4/5] Small build error --- src/frontend/cards/card-components.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/cards/card-components.tsx b/src/frontend/cards/card-components.tsx index d0150abe..4622fe95 100644 --- a/src/frontend/cards/card-components.tsx +++ b/src/frontend/cards/card-components.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Badge, Group, Menu, Table, Text } from "@mantine/core"; +import { Group, Menu, Table, Text } from "@mantine/core"; import { IconExternalLink, IconEyeOff, From 2b3df930a81808e62a6cd132eba033d04ece32ee Mon Sep 17 00:00:00 2001 From: Alex Kempen Date: Fri, 3 Jul 2026 19:02:33 -0500 Subject: [PATCH 5/5] Remove configuration path --- src/frontend/cards/card-components.tsx | 9 +++------ src/frontend/cards/insertable-card.tsx | 5 +---- src/frontend/common/url.tsx | 28 ++++++++------------------ src/shared/onshape-path.ts | 4 +++- 4 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/frontend/cards/card-components.tsx b/src/frontend/cards/card-components.tsx index 4622fe95..a773b484 100644 --- a/src/frontend/cards/card-components.tsx +++ b/src/frontend/cards/card-components.tsx @@ -14,7 +14,7 @@ import { AppContextMenu, MenuButton } from "../app-common/app-menu"; import { SearchHit } from "../search/search"; import { SearchHitTitle } from "../search/search-results"; import { CardThumbnail } from "../insert/thumbnail"; -import { InstancePath, isElementPath } from "../../shared/onshape-path"; +import { ConfigurablePath, InstancePath } from "../../shared/onshape-path"; import { openCannotDeriveAssemblyAlert } from "../app/alerts"; import { useInsertMutation, @@ -29,16 +29,13 @@ import { RequireAccessLevel } from "../api-utils/access-level"; import { useReloadThumbnailMutation } from "./card-hooks"; interface OpenDocumentItemsProps { - path: InstancePath; - configuration?: Configuration; + path: InstancePath | ConfigurablePath; } /** * Menu items which can be used to open or copy a link to a document. */ export function OpenDocumentItems(props: OpenDocumentItemsProps) { - const url = isElementPath(props.path) - ? makeUrl({ ...props.path, configuration: props.configuration }) - : makeUrl(props.path); + const url = makeUrl(props.path); return ( <> - + -): string { +export function makeUrl(path: DocumentPath): string { let url = `https://cad.onshape.com/documents/${path.documentId}`; if (isInstancePath(path)) { url += `/${path.instanceType}/${path.instanceId}`; @@ -28,37 +24,29 @@ export function makeUrl( if (isElementPath(path)) { url += `/e/${path.elementId}`; } - const config = - configuration ?? - (isConfigurablePath(path) ? path.configuration : undefined); - if (config) { - url += "?configuration=" + encodeConfigurationForQuery(config); + if (isConfigurablePath(path)) { + url += + "?configuration=" + encodeConfigurationForQuery(path.configuration); } return url; } -export interface ConfigurationPath { - configuration?: string; -} /** * Parses an Onshape document URL into an ElementPath. * Returns `undefined` if the URL could not be parsed successfully. */ -export function parseUrl( - urlString: string -): (ElementPath & ConfigurationPath) | undefined { +export function parseUrl(urlString: string): ElementPath | undefined { try { // Example pathname: /documents/769b556baf61d32b18813fd0/w/e6d6c2b3a472b97a7e352949/e/8a0c13d3b2b68a99502dc436 const url = new URL(urlString); const parts = url.pathname.split("/"); - const configuration = - url.searchParams.get("configuration") ?? undefined; + // const configuration = + // url.searchParams.get("configuration") ?? undefined; return { documentId: parts[2], instanceId: parts[4], instanceType: parts[3] as InstanceType, - elementId: parts[6], - configuration + elementId: parts[6] }; } catch { return undefined; diff --git a/src/shared/onshape-path.ts b/src/shared/onshape-path.ts index 46985d2c..df351b84 100644 --- a/src/shared/onshape-path.ts +++ b/src/shared/onshape-path.ts @@ -1,3 +1,5 @@ +import { Configuration } from "./configuration-models"; + export type InstanceType = "w" | "v" | "m"; export interface DocumentPath { @@ -19,7 +21,7 @@ export interface PartPath extends ElementPath { } export interface ConfigurablePath extends ElementPath { - configuration?: Record; + configuration: Configuration; } export function isDocumentPath(path: any): path is DocumentPath {