Organization and Pro tier link sharing features exposure (#6986)
* Improve url sharing for orgs and pros * Remove sharing via menu item * fmt * Not sure what's different but it is * fmt & lint * whoops * Update snapshots * Typos from codespell * Fix alignment * Update snapshots * Prune --------- Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
@ -197,18 +197,6 @@ test.describe(
|
||||
await clickElectronNativeMenuById(tronApp, 'File.Export current part')
|
||||
await cmdBar.expectCommandName('Export')
|
||||
})
|
||||
await test.step('Modeling.File.Share part via Zoo link', async () => {
|
||||
await page.waitForTimeout(250)
|
||||
await clickElectronNativeMenuById(
|
||||
tronApp,
|
||||
'File.Share part via Zoo link'
|
||||
)
|
||||
const textToCheck =
|
||||
'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!'
|
||||
// Check if text appears anywhere in the page
|
||||
const isTextVisible = page.getByText(textToCheck)
|
||||
await expect(isTextVisible).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
await test.step('Modeling.File.Preferences.Project settings', async () => {
|
||||
await page.waitForTimeout(250)
|
||||
await clickElectronNativeMenuById(
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
@ -2770,7 +2770,7 @@ mod clone_w_fillets {
|
||||
/// Test that KCL is executed correctly.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore] // turn on when https://github.com/KittyCAD/engine/pull/3380 is merged
|
||||
// Theres also a test in clone.rs you need to turn too
|
||||
// There's also a test in clone.rs you need to turn too
|
||||
async fn kcl_test_execute() {
|
||||
super::execute(TEST_NAME, true).await
|
||||
}
|
||||
|
@ -15,15 +15,9 @@ import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
||||
import usePlatform from '@src/hooks/usePlatform'
|
||||
import { APP_NAME } from '@src/lib/constants'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { copyFileShareLink } from '@src/lib/links'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import {
|
||||
codeManager,
|
||||
engineCommandManager,
|
||||
kclManager,
|
||||
} from '@src/lib/singletons'
|
||||
import { engineCommandManager, kclManager } from '@src/lib/singletons'
|
||||
import { type IndexLoaderData } from '@src/lib/types'
|
||||
import { useToken } from '@src/lib/singletons'
|
||||
import { commandBarActor } from '@src/lib/singletons'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
@ -108,7 +102,6 @@ function ProjectMenuPopover({
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const token = useToken()
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
const commands = useSelector(commandBarActor, commandsSelector)
|
||||
|
||||
@ -116,7 +109,6 @@ function ProjectMenuPopover({
|
||||
const insertCommandInfo = { name: 'Insert', groupId: 'code' }
|
||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
||||
const shareCommandInfo = { name: 'share-file-link', groupId: 'code' }
|
||||
const findCommand = (obj: { name: string; groupId: string }) =>
|
||||
Boolean(
|
||||
commands.find((c) => c.name === obj.name && c.groupId === obj.groupId)
|
||||
@ -217,19 +209,6 @@ function ProjectMenuPopover({
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'share-link',
|
||||
Element: 'button',
|
||||
children: 'Share part via Zoo link',
|
||||
disabled: !findCommand(shareCommandInfo),
|
||||
onClick: async () => {
|
||||
await copyFileShareLink({
|
||||
token: token ?? '',
|
||||
code: codeManager.code,
|
||||
name: project?.name || '',
|
||||
})
|
||||
},
|
||||
},
|
||||
'break',
|
||||
{
|
||||
id: 'go-home',
|
||||
|
@ -1,32 +1,93 @@
|
||||
import { err } from '@src/lib/trap'
|
||||
import { CustomIcon } from '@src/components/CustomIcon'
|
||||
import Tooltip from '@src/components/Tooltip'
|
||||
import usePlatform from '@src/hooks/usePlatform'
|
||||
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
|
||||
import { commandBarActor } from '@src/lib/singletons'
|
||||
import { billingActor, commandBarActor } from '@src/lib/singletons'
|
||||
import { useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { useKclContext } from '@src/lang/KclProvider'
|
||||
import { Tier } from '@src/machines/billingMachine'
|
||||
import type { SubscriptionsOrError } from '@src/machines/billingMachine'
|
||||
|
||||
const shareHotkey = 'mod+alt+s'
|
||||
const onShareClick = () =>
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'share-file-link', groupId: 'code' },
|
||||
})
|
||||
|
||||
const canPasswordProtectShareLinks = (
|
||||
subOrErr: undefined | SubscriptionsOrError
|
||||
): boolean => {
|
||||
if (subOrErr === undefined || typeof subOrErr === 'number' || err(subOrErr))
|
||||
return false
|
||||
return subOrErr.modeling_app.share_links[0] === 'password_protected'
|
||||
}
|
||||
|
||||
/** Share Zoo link button shown in the upper-right of the modeling view */
|
||||
export const ShareButton = () => {
|
||||
const platform = usePlatform()
|
||||
useHotkeys(shareHotkey, onShareClick, {
|
||||
|
||||
const [showOptions, setShowOptions] = useState(false)
|
||||
const [isRestrictedToOrg, setIsRestrictedToOrg] = useState(false)
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const billingContext = useSelector(billingActor, ({ context }) => context)
|
||||
|
||||
const allowOrgRestrict = billingContext.tier === Tier.Organization
|
||||
const allowPassword = canPasswordProtectShareLinks(
|
||||
billingContext.subscriptionsOrError
|
||||
)
|
||||
const hasOptions = allowOrgRestrict || allowPassword
|
||||
|
||||
// Prevents Organization and Pro tier users from one-click sharing,
|
||||
// and give them a chance to set a password and restrict to org.
|
||||
const onShareClickFreeOrUnknownRestricted = () => {
|
||||
if (hasOptions) {
|
||||
setShowOptions(true)
|
||||
return
|
||||
}
|
||||
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
name: 'share-file-link',
|
||||
groupId: 'code',
|
||||
isRestrictedToOrg: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onShareClickProOrOrganization = () => {
|
||||
setShowOptions(false)
|
||||
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
name: 'share-file-link',
|
||||
groupId: 'code',
|
||||
isRestrictedToOrg,
|
||||
password,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
useHotkeys(shareHotkey, onShareClickFreeOrUnknownRestricted, {
|
||||
scopes: ['modeling'],
|
||||
})
|
||||
|
||||
const kclContext = useKclContext()
|
||||
const disabled = kclContext.ast.body.some((n) => n.type === 'ImportStatement')
|
||||
|
||||
// It doesn't make sense for the user to be able to click on this
|
||||
// until we get what their subscription allows for.
|
||||
const disabled =
|
||||
kclContext.ast.body.some((n) => n.type === 'ImportStatement') ||
|
||||
billingContext.tier === undefined
|
||||
|
||||
return (
|
||||
<Popover className="relative flex">
|
||||
<Popover.Button className="relative group border-0 w-fit min-w-max p-0 rounded-l-full focus-visible:outline-appForeground">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onShareClick}
|
||||
onClick={onShareClickFreeOrUnknownRestricted}
|
||||
disabled={disabled}
|
||||
className="flex gap-1 items-center py-0 pl-0.5 pr-1.5 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 border border-solid active:border-primary"
|
||||
data-testid="share-button"
|
||||
@ -49,5 +110,59 @@ export const ShareButton = () => {
|
||||
)}
|
||||
</Tooltip>
|
||||
</button>
|
||||
</Popover.Button>
|
||||
{showOptions && (
|
||||
<Popover.Panel
|
||||
focus={true}
|
||||
className={`z-10 absolute top-full right-0 mt-1 pb-1 w-48 bg-chalkboard-10 dark:bg-chalkboard-90
|
||||
border border-solid border-chalkboard-20 dark:border-chalkboard-90 rounded
|
||||
shadow-lg`}
|
||||
>
|
||||
<div className="flex flex-col px-2">
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<CustomIcon name="lockClosed" className="w-6 h-6" />
|
||||
<input
|
||||
disabled={!allowPassword}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
className={`${allowPassword ? 'cursor-pointer' : 'cursor-not-allowed'} text-xs w-full py-1 bg-transparent text-chalkboard-100 placeholder:text-chalkboard-70 dark:text-chalkboard-10 dark:placeholder:text-chalkboard-50 focus:outline-none focus:ring-0`}
|
||||
type="text"
|
||||
placeholder="Set a password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="org-only"
|
||||
className="pl-1 inline-flex items-center"
|
||||
>
|
||||
<input
|
||||
disabled={!allowOrgRestrict}
|
||||
checked={isRestrictedToOrg}
|
||||
onChange={(_) => setIsRestrictedToOrg(!isRestrictedToOrg)}
|
||||
type="checkbox"
|
||||
name="org-only"
|
||||
className="form-checkbox"
|
||||
/>
|
||||
<span
|
||||
className={`text-xs ml-2 ${allowOrgRestrict ? 'cursor-pointer' : 'cursor-not-allowed text-chalkboard-50'}`}
|
||||
>
|
||||
Org. only access
|
||||
</span>
|
||||
</label>
|
||||
{!allowOrgRestrict && (
|
||||
<Tooltip>Upgrade to Organization to use this feature.</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<button disabled={disabled} onClick={onShareClickProOrOrganization}>
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ export function useCreateFileLinkQuery(
|
||||
decodeURIComponent(searchParams.get('code') ?? '')
|
||||
),
|
||||
name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
|
||||
isRestrictedToOrg: false,
|
||||
}
|
||||
|
||||
const argDefaultValues: CreateFileSchemaMethodOptional = {
|
||||
|
@ -32,6 +32,8 @@ interface KclCommandConfig {
|
||||
settings: {
|
||||
defaultUnit: UnitLength_type
|
||||
}
|
||||
isRestrictedToOrg?: boolean
|
||||
password?: string
|
||||
}
|
||||
|
||||
export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||
@ -175,11 +177,13 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||
groupId: 'code',
|
||||
needsReview: false,
|
||||
icon: 'link',
|
||||
onSubmit: () => {
|
||||
onSubmit: (input) => {
|
||||
copyFileShareLink({
|
||||
token: commandProps.authToken,
|
||||
code: codeManager.code,
|
||||
name: commandProps.projectData.project?.name || '',
|
||||
isRestrictedToOrg: input?.event.data.isRestrictedToOrg ?? false,
|
||||
password: input?.event.data.password,
|
||||
}).catch(reportRejection)
|
||||
},
|
||||
},
|
||||
|
@ -11,7 +11,7 @@ describe(`link creation tests`, () => {
|
||||
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
|
||||
const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&code=${expectedEncodedCode}&ask-open-desktop=true`
|
||||
|
||||
const result = createCreateFileUrl({ code, name })
|
||||
const result = createCreateFileUrl({ code, name, isRestrictedToOrg: false })
|
||||
expect(result.toString()).toBe(expectedLink)
|
||||
})
|
||||
})
|
||||
|
@ -11,6 +11,8 @@ import { err } from '@src/lib/trap'
|
||||
export interface FileLinkParams {
|
||||
code: string
|
||||
name: string
|
||||
isRestrictedToOrg: boolean
|
||||
password?: string
|
||||
}
|
||||
|
||||
export async function copyFileShareLink(
|
||||
@ -24,7 +26,12 @@ export async function copyFileShareLink(
|
||||
return
|
||||
}
|
||||
const shareUrl = createCreateFileUrl(args)
|
||||
const shortlink = await createShortlink(token, shareUrl.toString())
|
||||
const shortlink = await createShortlink(
|
||||
token,
|
||||
shareUrl.toString(),
|
||||
args.isRestrictedToOrg,
|
||||
args.password
|
||||
)
|
||||
|
||||
if (err(shortlink)) {
|
||||
toast.error(shortlink.message, {
|
||||
@ -70,23 +77,32 @@ export function createCreateFileUrl({ code, name }: FileLinkParams) {
|
||||
*/
|
||||
export async function createShortlink(
|
||||
token: string,
|
||||
url: string
|
||||
url: string,
|
||||
isRestrictedToOrg: boolean,
|
||||
password?: string
|
||||
): Promise<Error | { key: string; url: string }> {
|
||||
/**
|
||||
* We don't use our `withBaseURL` function here because
|
||||
* there is no URL shortener service in the dev API.
|
||||
*/
|
||||
const body: {
|
||||
url: string
|
||||
restrict_to_org: boolean
|
||||
password?: string
|
||||
} = {
|
||||
url,
|
||||
restrict_to_org: isRestrictedToOrg,
|
||||
}
|
||||
if (password) {
|
||||
body.password = password
|
||||
}
|
||||
const response = await fetch(`${VITE_KC_API_BASE_URL}/user/shortlinks`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url,
|
||||
// In future we can support org-scoped and password-protected shortlinks here
|
||||
// https://zoo.dev/docs/api/shortlinks/create-a-shortlink-for-a-user?lang=typescript
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
|
@ -14,11 +14,34 @@ export enum BillingTransition {
|
||||
Wait = 'wait',
|
||||
}
|
||||
|
||||
// It's nice to be explicit if we are an Organization, Pro, Free.
|
||||
// @kittycad/lib offers some types around this, but they aren't as...
|
||||
// homogeneous: Models['ZooProductSubscriptions_type'], and
|
||||
// Models['Org_type'].
|
||||
export enum Tier {
|
||||
Free = 'free',
|
||||
Pro = 'pro',
|
||||
Organization = 'organization',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export type OrgOrError = Models['Org_type'] | number | Error
|
||||
export type SubscriptionsOrError =
|
||||
| Models['ZooProductSubscriptions_type']
|
||||
| number
|
||||
| Error
|
||||
export type TierBasedOn = {
|
||||
orgOrError: OrgOrError
|
||||
subscriptionsOrError: SubscriptionsOrError
|
||||
}
|
||||
|
||||
export interface BillingContext {
|
||||
credits: undefined | number
|
||||
allowance: undefined | number
|
||||
error: undefined | Error
|
||||
urlUserService: string
|
||||
tier: undefined | Tier
|
||||
subscriptionsOrError: undefined | SubscriptionsOrError
|
||||
}
|
||||
|
||||
export interface BillingUpdateEvent {
|
||||
@ -30,9 +53,30 @@ export const BILLING_CONTEXT_DEFAULTS: BillingContext = Object.freeze({
|
||||
credits: undefined,
|
||||
allowance: undefined,
|
||||
error: undefined,
|
||||
tier: undefined,
|
||||
subscriptionsOrError: undefined,
|
||||
urlUserService: '',
|
||||
})
|
||||
|
||||
const toTierFrom = (args: TierBasedOn): Tier => {
|
||||
if (typeof args.orgOrError !== 'number' && !err(args.orgOrError)) {
|
||||
return Tier.Organization
|
||||
} else if (
|
||||
typeof args.subscriptionsOrError !== 'number' &&
|
||||
!err(args.subscriptionsOrError)
|
||||
) {
|
||||
const subscriptions: Models['ZooProductSubscriptions_type'] =
|
||||
args.subscriptionsOrError
|
||||
if (subscriptions.modeling_app.name === 'pro') {
|
||||
return Tier.Pro
|
||||
} else {
|
||||
return Tier.Free
|
||||
}
|
||||
}
|
||||
|
||||
return Tier.Unknown
|
||||
}
|
||||
|
||||
export const billingMachine = setup({
|
||||
types: {
|
||||
context: {} as BillingContext,
|
||||
@ -72,33 +116,45 @@ export const billingMachine = setup({
|
||||
input.event.apiToken
|
||||
)
|
||||
|
||||
const tier = toTierFrom({
|
||||
orgOrError,
|
||||
subscriptionsOrError,
|
||||
})
|
||||
|
||||
let credits =
|
||||
Number(billing.monthly_api_credits_remaining) +
|
||||
Number(billing.stable_api_credits_remaining)
|
||||
let allowance = undefined
|
||||
|
||||
// If user is part of an org, the endpoint will return data.
|
||||
if (typeof orgOrError !== 'number' && !err(orgOrError)) {
|
||||
switch (tier) {
|
||||
case Tier.Organization:
|
||||
case Tier.Pro:
|
||||
credits = Infinity
|
||||
// Otherwise they are on a Pro or Free subscription
|
||||
} else if (
|
||||
break
|
||||
case Tier.Free:
|
||||
// TS too dumb Tier.Free has the same logic
|
||||
if (
|
||||
typeof subscriptionsOrError !== 'number' &&
|
||||
!err(subscriptionsOrError)
|
||||
) {
|
||||
const subscriptions: Models['ZooProductSubscriptions_type'] =
|
||||
subscriptionsOrError
|
||||
if (subscriptions.modeling_app.name === 'pro') {
|
||||
credits = Infinity
|
||||
} else {
|
||||
allowance = Number(
|
||||
subscriptions.modeling_app.monthly_pay_as_you_go_api_credits
|
||||
subscriptionsOrError.modeling_app
|
||||
.monthly_pay_as_you_go_api_credits
|
||||
)
|
||||
}
|
||||
break
|
||||
case Tier.Unknown:
|
||||
break
|
||||
default:
|
||||
const _exh: never = tier
|
||||
}
|
||||
|
||||
// If nothing matches, we show a credit total.
|
||||
|
||||
return {
|
||||
error: undefined,
|
||||
tier,
|
||||
subscriptionsOrError,
|
||||
credits,
|
||||
allowance,
|
||||
}
|
||||
|
@ -66,6 +66,10 @@ export type CommandBarMachineEvent =
|
||||
name: string
|
||||
groupId: string
|
||||
argDefaultValues?: { [x: string]: unknown }
|
||||
|
||||
// I'm sorry but the way we did share URL called for this.
|
||||
isRestrictedToOrg?: boolean
|
||||
password?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
@ -115,7 +119,7 @@ export const commandBarMachine = setup({
|
||||
}
|
||||
selectedCommand?.onSubmit(resolvedArgs)
|
||||
} else {
|
||||
selectedCommand?.onSubmit()
|
||||
selectedCommand?.onSubmit({ context, event })
|
||||
}
|
||||
},
|
||||
'Clear selected command': assign({
|
||||
|
@ -26,7 +26,6 @@ export type MenuLabels =
|
||||
| 'File.Create new folder'
|
||||
| 'File.Add file to project'
|
||||
| 'File.Export current part'
|
||||
| 'File.Share part via Zoo link'
|
||||
| 'File.Preferences.Project settings'
|
||||
| 'Design.Start sketch'
|
||||
| 'Design.Create an offset plane'
|
||||
|
@ -184,15 +184,6 @@ export const modelingFileRole = (
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Share part via Zoo link',
|
||||
id: 'File.Share part via Zoo link',
|
||||
click: () => {
|
||||
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||
menuLabel: 'File.Share part via Zoo link',
|
||||
})
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Preferences',
|
||||
|
@ -1,13 +1,8 @@
|
||||
import { AxisNames } from '@src/lib/constants'
|
||||
import { copyFileShareLink } from '@src/lib/links'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import type { Project } from '@src/lib/project'
|
||||
import type { SettingsType } from '@src/lib/settings/initialSettings'
|
||||
import {
|
||||
codeManager,
|
||||
engineCommandManager,
|
||||
sceneInfra,
|
||||
} from '@src/lib/singletons'
|
||||
import { engineCommandManager, sceneInfra } from '@src/lib/singletons'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import { uuidv4 } from '@src/lib/utils'
|
||||
import { authActor, settingsActor } from '@src/lib/singletons'
|
||||
@ -84,12 +79,6 @@ export function modelingMenuCallbackMostActions(
|
||||
})
|
||||
} else if (data.menuLabel === 'File.Preferences.Theme color') {
|
||||
navigate(filePath + PATHS.SETTINGS_USER + '#themeColor')
|
||||
} else if (data.menuLabel === 'File.Share part via Zoo link') {
|
||||
copyFileShareLink({
|
||||
token: token ?? '',
|
||||
code: codeManager.code,
|
||||
name: project?.name || '',
|
||||
}).catch(reportRejection)
|
||||
} else if (data.menuLabel === 'File.Preferences.User default units') {
|
||||
navigate(filePath + PATHS.SETTINGS_USER + '#defaultUnit')
|
||||
} else if (data.menuLabel === 'File.Add file to project') {
|
||||
|