Compare commits

...

13 Commits

Author SHA1 Message Date
df167c0382 WIP 2025-05-14 14:18:57 -05:00
15857e9191 Change sweep relativeTo flag defaults
Default to trajectoryCurve, as Ryan wants
2025-05-14 14:18:11 -05:00
1a32f664d0 Change sweep relativeTo flag defaults
Default to trajectoryCurve, as Ryan wants
2025-05-14 13:57:12 -05:00
5c2dfb8e40 Support new sweep flag (#6932)
Closes https://github.com/KittyCAD/engine/issues/3115
2025-05-14 13:54:10 -05:00
0e341d7863 #6202 Save input value before closing settings dialogue (#6931)
* call blur on current input before closing settings dialogue to save value

* separate esc handling is not needed

* lint
2025-05-14 14:16:23 -04:00
6a03ff9596 Stop checking for intermediate export toasts (#6935) 2025-05-14 17:53:44 +00:00
d7bd0c937d Keep test toast messages around for longer (#6930)
* Keep test toast messages around for longer

* Check for at least two locators

I wasn't able to reproduce, but it's possible one stuck around from a previous test.
2025-05-14 12:24:38 -04:00
d3b2483f4f Clear errors when leaving file to avoid seeing previous files errors (#6928)
Clear errors when leaving file to avoid seeing previous files errors when opening new project

Co-authored-by: Jace Browning <jacebrowning@gmail.com>
2025-05-14 17:20:04 +02:00
7838b7c9fd Fix "include settings" setting to have an effect (#6917)
* Fix "include settings" setting to have an effect

I'm not sold on if we should have this setting, but this fixes it for
now. The issue is was that the new callback actor approach was using a
stale version of the settings every time it received an "update" event:
JS closure problems. Now it receives the new settings as an event
payload.

* Update src/machines/settingsMachine.ts

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-05-14 15:14:33 +00:00
130ecf1f88 Shard macOS and Windows e2e tests (#6926) 2025-05-14 14:33:53 +00:00
550d8b3753 #6300 Fix wrong Feature Tree when switching to kcl file with errors (#6922)
* reset lastSuccessfulOperation, variables when switching to new kcl file, add test

* use scene.settled instead of random delay in test
2025-05-14 14:17:22 +00:00
696222a070 Change KCL error messages to display principal type of values (#6906) 2025-05-14 10:04:51 -04:00
edb424988d Restore the native file menu tests (#6279)
* Restore the native file menu tests

* fix: saving off progress

* chore: making progress cleaning up these verbose tests and improving app logic for e2e

* chore: rewriting tests

* fix: reworking application logic for file menu in the scene and e2e scene file menu test

* chore: updating more e2e tests

* fix: updated all the tests, auto fixers

* fix: trying to improve tests within E2E, they aren't failing locally even with --repeat-each=10

* fix: application logic has a bug that you can navigate instantly but the scroll to view code will not trigger which breaks end to end tests

* fix: improving E2E tests

* fix: fixing clipboard typo

* fix: porting test() for each native file menu to a test.step to speed it up

* fix: auto fixes and console log helper function for playwright runtimes

* fix: more cleanup

* fix: trying to fix these...

* fix: got the tests working

* fix: addressing PR comments

* fix: trying to stablize the tests

* fix: auto fixes

* fix: trying to make it the command name and not arg? could be a source of race condition if the input is not written fast enough?

* fix: maybe because this close locator was running too quickly?

* fix: panic timeout, classic

* fix: these are gone

* fix: shorter waits

---------

Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-05-14 09:06:29 -04:00
41 changed files with 2666 additions and 2465 deletions

View File

@ -234,10 +234,16 @@ jobs:
shardTotal: 8
- os: namespace-profile-macos-8-cores
shardIndex: 1
shardTotal: 1
shardTotal: 2
- os: namespace-profile-macos-8-cores
shardIndex: 2
shardTotal: 2
- os: windows-latest-8-cores
shardIndex: 1
shardTotal: 1
shardTotal: 2
- os: windows-latest-8-cores
shardIndex: 2
shardTotal: 2
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@ sweep(
path: Sketch | Helix,
sectional?: bool,
tolerance?: number,
relativeTo?: string,
tagStart?: TagDeclarator,
tagEnd?: TagDeclarator,
): [Solid]
@ -30,6 +31,7 @@ You can provide more than one sketch to sweep, and they will all be swept along
| `path` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) or [`Helix`](/docs/kcl-std/types/std-types-Helix) | The path to sweep the sketch along | Yes |
| `sectional` | [`bool`](/docs/kcl-std/types/std-types-bool) | If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
| `tolerance` | [`number`](/docs/kcl-std/types/std-types-number) | Tolerance for this operation | No |
| `relativeTo` | [`string`](/docs/kcl-std/types/std-types-string) | What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to sketchPlane. | No |
| `tagStart` | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | A named tag for the face at the start of the sweep, i.e. the original sketch | No |
| `tagEnd` | [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator) | A named tag for the face at the end of the sweep | No |

View File

@ -58,12 +58,6 @@ test(
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
// Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
// Expect it to succeed
const errorToastMessage = page.getByText(`Error while exporting`)
const engineErrorToastMessage = page.getByText(`Nothing to export`)
@ -72,7 +66,6 @@ test(
const successToastMessage = page.getByText(`Exported successfully`)
await expect(successToastMessage).toBeVisible()
await expect(exportingToastMessage).not.toBeVisible()
// Check for the exported file
const firstFileFullPath = path.resolve(

View File

@ -155,6 +155,12 @@ export class CmdBarFixture {
}
}
closeCmdBar = async () => {
const cmdBarCloseBtn = this.page.getByTestId('command-bar-close-button')
await cmdBarCloseBtn.click()
await expect(this.cmdBarElement).not.toBeVisible()
}
get cmdSearchInput() {
return this.page.getByTestId('cmd-bar-search')
}
@ -298,4 +304,27 @@ export class CmdBarFixture {
`Monitoring text-to-cad API requests. Output will be saved to: ${outputPath}`
)
}
async toBeOpened() {
// Check that the command bar is opened
await expect(this.cmdBarElement).toBeVisible({ timeout: 10_000 })
}
async expectArgValue(value: string) {
// Check the placeholder project name exists
const actualArgument = await this.cmdBarElement
.getByTestId('cmd-bar-arg-value')
.inputValue()
const expectedArgument = value
expect(actualArgument).toBe(expectedArgument)
}
async expectCommandName(value: string) {
// Check the placeholder project name exists
const actual = await this.cmdBarElement
.getByTestId('command-name')
.textContent()
const expected = value
expect(actual).toBe(expected)
}
}

View File

@ -24,6 +24,7 @@ export class HomePageFixture {
projectTextName!: Locator
sortByDateBtn!: Locator
sortByNameBtn!: Locator
appHeader!: Locator
tutorialBtn!: Locator
constructor(page: Page) {
@ -44,6 +45,7 @@ export class HomePageFixture {
this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
this.appHeader = this.page.getByTestId('app-header')
this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
}
@ -125,4 +127,11 @@ export class HomePageFixture {
await this.createAndGoToProject(name)
}
isNativeFileMenuCreated = async () => {
await expect(this.appHeader).toHaveAttribute(
'data-native-file-menu',
'true'
)
}
}

View File

@ -46,6 +46,7 @@ export class SceneFixture {
public networkToggleConnected!: Locator
public engineConnectionsSpinner!: Locator
public startEditSketchBtn!: Locator
public appHeader!: Locator
constructor(page: Page) {
this.page = page
@ -57,6 +58,7 @@ export class SceneFixture {
this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' }))
this.appHeader = this.page.getByTestId('app-header')
}
private _serialiseScene = async (): Promise<SceneSerialised> => {
const camera = await this.getCameraInfo()
@ -280,6 +282,13 @@ export class SceneFixture {
await expect(buttonToTest).toBeVisible()
await buttonToTest.click()
}
isNativeFileMenuCreated = async () => {
await expect(this.appHeader).toHaveAttribute(
'data-native-file-menu',
'true'
)
}
}
function isColourArray(

File diff suppressed because it is too large Load Diff

View File

@ -545,7 +545,8 @@ extrude002 = extrude(profile002, length = 150)
expect(alreadyExportingToastMessage).not.toBeVisible(),
])
await expect(successToastMessage).toHaveCount(2)
const count = await successToastMessage.count()
await expect(count).toBeGreaterThanOrEqual(2)
})
})

View File

@ -3496,6 +3496,73 @@ profile001 = startProfile(sketch001, at = [-102.72, 237.44])
).toBeVisible()
})
// Ensure feature tree is not showing previous file's content when switching to a file with KCL errors.
test('Feature tree shows correct sketch count per file', async ({
context,
homePage,
scene,
toolbar,
cmdBar,
page,
}) => {
const u = await getUtils(page)
// Setup project with files.
const GOOD_KCL = `sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [220.81, 253.8])
|> line(end = [132.84, -151.31])
|> line(end = [25.51, 167.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
sketch002 = startSketchOn(XZ)
profile002 = startProfile(sketch002, at = [158.35, -70.82])
|> line(end = [73.9, -152.19])
|> line(end = [85.33, 135.48])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()`
const ERROR_KCL = `sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [127.56, 179.02])
|> line(end = [132.84, -112.6])
|> line(end = [85.33, 234.01])
|> line(enfd = [-137.23, -54.55])`
await context.folderSetupFn(async (dir) => {
const projectDir = path.join(dir, 'multi-file-sketch-test')
await fs.mkdir(projectDir, { recursive: true })
await Promise.all([
fs.writeFile(path.join(projectDir, 'good.kcl'), GOOD_KCL, 'utf-8'),
fs.writeFile(path.join(projectDir, 'error.kcl'), ERROR_KCL, 'utf-8'),
])
})
await page.setBodyDimensions({ width: 1000, height: 800 })
await homePage.openProject('multi-file-sketch-test')
await scene.connectionEstablished()
await scene.settled(cmdBar)
await u.closeDebugPanel()
await toolbar.openFeatureTreePane()
await toolbar.openPane('files')
await toolbar.openFile('good.kcl')
await expect(
toolbar.featureTreePane.getByRole('button', { name: 'Sketch' })
).toHaveCount(2)
await toolbar.openFile('error.kcl')
// Ensure filetree is populated
await scene.settled(cmdBar)
await expect(
toolbar.featureTreePane.getByRole('button', { name: 'Sketch' })
).toHaveCount(0)
})
test('adding a syntax error, recovers after fixing', async ({
page,
homePage,

View File

@ -21,6 +21,7 @@ export const token = process.env.token || ''
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'
import type { ElectronZoo } from '@e2e/playwright/fixtures/fixtureSetup'
import { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
import { test } from '@e2e/playwright/zoo-test'
@ -1149,3 +1150,77 @@ export function perProjectSettingsToToml(
// eslint-disable-next-line no-restricted-syntax
return TOML.stringify(settings as any)
}
export async function clickElectronNativeMenuById(
tronApp: ElectronZoo,
menuId: string
) {
const clickWasTriggered = await tronApp.electron.evaluate(
async ({ app }, menuId) => {
if (!app || !app.applicationMenu) {
return false
}
const menu = app.applicationMenu.getMenuItemById(menuId)
if (!menu) return false
menu.click()
return true
},
menuId
)
expect(clickWasTriggered).toBe(true)
}
export async function findElectronNativeMenuById(
tronApp: ElectronZoo,
menuId: string
) {
const found = await tronApp.electron.evaluate(async ({ app }, menuId) => {
if (!app || !app.applicationMenu) {
return false
}
const menu = app.applicationMenu.getMenuItemById(menuId)
if (!menu) return false
return true
}, menuId)
expect(found).toBe(true)
}
export async function openSettingsExpectText(page: Page, text: string) {
const settings = page.getByTestId('settings-dialog-panel')
await expect(settings).toBeVisible()
// You are viewing the user tab
const actualText = settings.getByText(text)
await expect(actualText).toBeVisible()
}
export async function openSettingsExpectLocator(page: Page, selector: string) {
const settings = page.getByTestId('settings-dialog-panel')
await expect(settings).toBeVisible()
// You are viewing the keybindings tab
const settingsLocator = settings.locator(selector)
await expect(settingsLocator).toBeVisible()
}
/**
* A developer helper function to make playwright send all the console logs to stdout
* Call this within your E2E test and pass in the page or the tronApp to get as many
* logs piped to stdout for debugging
*/
export async function enableConsoleLogEverything({
page,
tronApp,
}: { page?: Page; tronApp?: ElectronZoo }) {
page?.on('console', (msg) => {
console.log(`[Page-log]: ${msg.text()}`)
})
tronApp?.electron.on('window', async (electronPage) => {
electronPage.on('console', (msg) => {
console.log(`[Renderer] ${msg.type()}: ${msg.text()}`)
})
})
tronApp?.electron.on('console', (msg) => {
console.log(`[Main] ${msg.type()}: ${msg.text()}`)
})
}

18
rust/Cargo.lock generated
View File

@ -535,7 +535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -963,7 +963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -1746,7 +1746,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -2080,9 +2080,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.120"
version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48b71e06ee5d711d0085864a756fb6a304531246689ea00c6ef5d740670c3701"
checksum = "94ba95c22493d79ec8a1faab963d8903f6de0e373efedf2bc3bb76a0ddbab036"
dependencies = [
"anyhow",
"chrono",
@ -2987,7 +2987,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -3306,7 +3306,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -3900,7 +3900,7 @@ dependencies = [
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@ -4753,7 +4753,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]

View File

@ -1249,7 +1249,7 @@ secondSketch = startSketchOn(part001, face = '')
let err = err.as_kcl_error().unwrap();
assert_eq!(
err.message(),
"The arg face was given, but it was the wrong type. It should be type FaceTag but it was string (text)"
"The arg face was given, but it was the wrong type. It should be type FaceTag but it was string"
);
}
@ -1882,7 +1882,7 @@ someFunction('INVALID')
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([46, 55, 0]), SourceRange([60, 83, 0])], message: "This function expected the input argument to be Solid or Plane but it's actually of type string (text)" }"#
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([46, 55, 0]), SourceRange([60, 83, 0])], message: "This function expected the input argument to be Solid or Plane but it's actually of type string" }"#
);
}

View File

@ -913,11 +913,9 @@ impl Node<MemberExpression> {
}),
(being_indexed, _, _) => {
let t = being_indexed.human_friendly_type();
let article = article_for(t);
let article = article_for(&t);
Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only arrays and objects can be indexed, but you're trying to index {article} {t}"
),
message: format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
source_ranges: vec![self.clone().into()],
}))
}
@ -1698,8 +1696,9 @@ impl Node<ObjectExpression> {
}
}
fn article_for(s: &str) -> &'static str {
if s.starts_with(['a', 'e', 'i', 'o', 'u']) {
fn article_for<S: AsRef<str>>(s: S) -> &'static str {
// '[' is included since it's an array.
if s.as_ref().starts_with(['a', 'e', 'i', 'o', 'u', '[']) {
"an"
} else {
"a"
@ -1709,10 +1708,9 @@ fn article_for(s: &str) -> &'static str {
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
v.as_ty_f64().ok_or_else(|| {
let actual_type = v.human_friendly_type();
let article = article_for(actual_type);
KclError::Semantic(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Expected a number, but found {article} {actual_type}",),
message: format!("Expected a number, but found {actual_type}",),
})
})
}
@ -2446,19 +2444,23 @@ arr1 = [42]: [number(cm)]
a = 42: string
"#;
let result = parse_execute(program).await;
assert!(result
.unwrap_err()
.to_string()
.contains("could not coerce number value to type string"));
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce number(default units) value to type string"),
"Expected error but found {err:?}"
);
let program = r#"
a = 42: Plane
"#;
let result = parse_execute(program).await;
assert!(result
.unwrap_err()
.to_string()
.contains("could not coerce number value to type Plane"));
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce number(default units) value to type Plane"),
"Expected error but found {err:?}"
);
let program = r#"
arr = [0]: [string]
@ -2467,7 +2469,7 @@ arr = [0]: [string]
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce array (list) value to type [string]"),
.contains("could not coerce [any; 1] value to type [string]"),
"Expected error but found {err:?}"
);
@ -2478,7 +2480,7 @@ mixedArr = [0, "a"]: [number(mm)]
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce array (list) value to type [number(mm)]"),
.contains("could not coerce [any; 2] value to type [number(mm)]"),
"Expected error but found {err:?}"
);
}

View File

@ -280,7 +280,10 @@ impl KclValue {
/// Human readable type name used in error messages. Should not be relied
/// on for program logic.
pub(crate) fn human_friendly_type(&self) -> &'static str {
pub(crate) fn human_friendly_type(&self) -> String {
if let Some(t) = self.principal_type() {
return t.to_string();
}
match self {
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
@ -314,6 +317,7 @@ impl KclValue {
KclValue::Type { .. } => "type",
KclValue::KclNone { .. } => "None",
}
.to_owned()
}
pub(crate) fn from_literal(literal: Node<Literal>, exec_state: &mut ExecState) -> Self {

View File

@ -1910,13 +1910,13 @@ notNull = !myNull
"#;
assert_eq!(
parse_execute(code1).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number",
"Cannot apply unary operator ! to non-boolean value: number(default units)",
);
let code2 = "notZero = !0";
assert_eq!(
parse_execute(code2).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number",
"Cannot apply unary operator ! to non-boolean value: number(default units)",
);
let code3 = r#"
@ -1924,7 +1924,7 @@ notEmptyString = !""
"#;
assert_eq!(
parse_execute(code3).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: string (text)",
"Cannot apply unary operator ! to non-boolean value: string",
);
let code4 = r#"
@ -1933,7 +1933,7 @@ notMember = !obj.a
"#;
assert_eq!(
parse_execute(code4).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number",
"Cannot apply unary operator ! to non-boolean value: number(default units)",
);
let code5 = "
@ -1941,7 +1941,7 @@ a = []
notArray = !a";
assert_eq!(
parse_execute(code5).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: array (list)",
"Cannot apply unary operator ! to non-boolean value: [any; 0]",
);
let code6 = "
@ -1949,7 +1949,7 @@ x = {}
notObject = !x";
assert_eq!(
parse_execute(code6).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: object",
"Cannot apply unary operator ! to non-boolean value: { }",
);
let code7 = "
@ -1975,7 +1975,7 @@ notTagDeclarator = !myTagDeclarator";
assert!(
tag_declarator_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: TagDeclarator"),
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
"Actual error: {:?}",
tag_declarator_err
);
@ -1989,7 +1989,7 @@ notTagIdentifier = !myTag";
assert!(
tag_identifier_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: TagIdentifier"),
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
"Actual error: {:?}",
tag_identifier_err
);

View File

@ -257,14 +257,22 @@ impl Args {
};
let arg = arg.value.coerce(ty, exec_state).map_err(|_| {
let actual_type_name = arg.value.human_friendly_type();
let actual_type = arg.value.principal_type();
let actual_type_name = actual_type
.as_ref()
.map(|t| t.to_string())
.unwrap_or_else(|| arg.value.human_friendly_type().to_owned());
let msg_base = format!(
"This function expected the input argument to be {} but it's actually of type {actual_type_name}",
ty.human_friendly_type(),
);
let suggestion = match (ty, actual_type_name) {
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER),
(RuntimeType::Array(t, _), "Sketch") if **t == RuntimeType::Primitive(PrimitiveType::Solid) => {
let suggestion = match (ty, actual_type) {
(RuntimeType::Primitive(PrimitiveType::Solid), Some(RuntimeType::Primitive(PrimitiveType::Sketch))) => {
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
}
(RuntimeType::Array(t, _), Some(RuntimeType::Primitive(PrimitiveType::Sketch)))
if **t == RuntimeType::Primitive(PrimitiveType::Solid) =>
{
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
}
_ => None,
@ -381,14 +389,22 @@ impl Args {
}))?;
let arg = arg.value.coerce(ty, exec_state).map_err(|_| {
let actual_type_name = arg.value.human_friendly_type();
let actual_type = arg.value.principal_type();
let actual_type_name = actual_type
.as_ref()
.map(|t| t.to_string())
.unwrap_or_else(|| arg.value.human_friendly_type().to_owned());
let msg_base = format!(
"This function expected the input argument to be {} but it's actually of type {actual_type_name}",
ty.human_friendly_type(),
);
let suggestion = match (ty, actual_type_name) {
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER),
(RuntimeType::Array(ty, _), "Sketch") if **ty == RuntimeType::Primitive(PrimitiveType::Solid) => {
let suggestion = match (ty, actual_type) {
(RuntimeType::Primitive(PrimitiveType::Solid), Some(RuntimeType::Primitive(PrimitiveType::Sketch))) => {
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
}
(RuntimeType::Array(ty, _), Some(RuntimeType::Primitive(PrimitiveType::Sketch)))
if **ty == RuntimeType::Primitive(PrimitiveType::Solid) =>
{
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
}
_ => None,

View File

@ -3,7 +3,7 @@
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, ModelingCmd};
use kittycad_modeling_cmds::{self as kcmc};
use kittycad_modeling_cmds::{self as kcmc, shared::RelativeTo};
use schemars::JsonSchema;
use serde::Serialize;
@ -37,11 +37,20 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
)?;
let sectional = args.get_kw_arg_opt("sectional")?;
let tolerance: Option<TyF64> = args.get_kw_arg_opt_typed("tolerance", &RuntimeType::length(), exec_state)?;
let relative_to: Option<String> = args.get_kw_arg_opt_typed("relativeTo", &RuntimeType::string(), exec_state)?;
let tag_start = args.get_kw_arg_opt("tagStart")?;
let tag_end = args.get_kw_arg_opt("tagEnd")?;
let value = inner_sweep(
sketches, path, sectional, tolerance, tag_start, tag_end, exec_state, args,
sketches,
path,
sectional,
tolerance,
relative_to,
tag_start,
tag_end,
exec_state,
args,
)
.await?;
Ok(value.into())
@ -158,6 +167,7 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
path = { docs = "The path to sweep the sketch along" },
sectional = { docs = "If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components." },
tolerance = { docs = "Tolerance for this operation" },
relative_to = { docs = "What is the sweep relative to? Can be either 'sketchPlane' or 'trajectoryCurve'. Defaults to trajectoryCurve."},
tag_start = { docs = "A named tag for the face at the start of the sweep, i.e. the original sketch" },
tag_end = { docs = "A named tag for the face at the end of the sweep" },
},
@ -169,6 +179,7 @@ async fn inner_sweep(
path: SweepPath,
sectional: Option<bool>,
tolerance: Option<TyF64>,
relative_to: Option<String>,
tag_start: Option<TagNode>,
tag_end: Option<TagNode>,
exec_state: &mut ExecState,
@ -178,6 +189,16 @@ async fn inner_sweep(
SweepPath::Sketch(sketch) => sketch.id.into(),
SweepPath::Helix(helix) => helix.value.into(),
};
let relative_to = match relative_to.as_deref() {
Some("sketchPlane") => RelativeTo::SketchPlane,
Some("trajectoryCurve") | None => RelativeTo::TrajectoryCurve,
Some(_) => {
return Err(KclError::Syntax(crate::errors::KclErrorDetails {
source_ranges: vec![args.source_range],
message: "If you provide relativeTo, it must either be 'sketchPlane' or 'trajectoryCurve'".to_owned(),
}))
}
};
let mut solids = Vec::new();
for sketch in &sketches {
@ -189,6 +210,7 @@ async fn inner_sweep(
trajectory,
sectional: sectional.unwrap_or(false),
tolerance: LengthUnit(tolerance.as_ref().map(|t| t.to_mm()).unwrap_or(DEFAULT_TOLERANCE)),
relative_to,
}),
)
.await?;

View File

@ -4,8 +4,7 @@ description: Error from executing argument_error.kcl
---
KCL Semantic error
× semantic: f requires a value with type `fn(any): any`, but found array
│ (list)
× semantic: f requires a value with type `fn(any): any`, but found [any; 2]
╭─[5:1]
4 │
5 │ map(f, f = [0, 1])
@ -16,7 +15,7 @@ KCL Semantic error
╰─▶ KCL Semantic error
× semantic: f requires a value with type `fn(any): any`, but found
array (list)
[any; 2]
╭─[5:12]
4 │
5 │ map(f, f = [0, 1])

View File

@ -5,7 +5,7 @@ description: Error from executing array_elem_pop_empty_fail.kcl
KCL Semantic error
× semantic: The input argument of `std::array::pop` requires a value with
│ type `[any; 1+]`, but found array (list)
│ type `[any; 1+]`, but found [any; 0]
╭─[2:8]
1 │ arr = []
2 │ fail = pop(arr)
@ -16,7 +16,7 @@ KCL Semantic error
╰─▶ KCL Semantic error
× semantic: The input argument of `std::array::pop` requires a value
│ with type `[any; 1+]`, but found array (list)
│ with type `[any; 1+]`, but found [any; 0]
╭─[2:12]
1 │ arr = []
2 │ fail = pop(arr)

View File

@ -4,7 +4,7 @@ description: Error from executing comparisons_multiple.kcl
---
KCL Semantic error
× semantic: Expected a number, but found a boolean (true/false value)
× semantic: Expected a number, but found bool
╭────
1 │ assert(3 == 3 == 3, error = "this should not compile")
· ───┬──

View File

@ -5,7 +5,7 @@ description: Error from executing error_inside_fn_also_has_source_range_of_call_
KCL Semantic error
× semantic: This function expected the input argument to be Solid or Plane
│ but it's actually of type string (text)
│ but it's actually of type string
╭─[3:23]
2 │ fn someNestedFunction(@something2) {
3 │ startSketchOn(something2)
@ -25,7 +25,7 @@ KCL Semantic error
├─▶ KCL Semantic error
× semantic: This function expected the input argument to be Solid or
│ │ Plane but it's actually of type string (text)
│ │ Plane but it's actually of type string
│ ╭─[3:23]
│ 2 │ fn someNestedFunction(@something2) {
│ 3 │ startSketchOn(something2)
@ -37,7 +37,7 @@ KCL Semantic error
╰─▶ KCL Semantic error
× semantic: This function expected the input argument to be Solid or
│ Plane but it's actually of type string (text)
│ Plane but it's actually of type string
╭─[6:5]
5 │
6 │ someNestedFunction(something)

View File

@ -1,11 +1,11 @@
---
source: kcl/src/simulation_tests.rs
source: kcl-lib/src/simulation_tests.rs
description: Error from executing invalid_member_object.kcl
---
KCL Semantic error
× semantic: Only arrays and objects can be indexed, but you're trying to
index a number
× semantic: Only arrays can be indexed, but you're trying to index a
number(default units)
╭─[2:5]
1 │ num = 999
2 │ x = num[3]

View File

@ -5121,7 +5121,8 @@ description: Artifact commands bench.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{
@ -5132,7 +5133,8 @@ description: Artifact commands bench.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
}
]

View File

@ -905,7 +905,8 @@ description: Artifact commands cold-plate.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{

View File

@ -162,7 +162,7 @@ description: Variables in memory after executing countersunk-plate.kcl
],
"tag": null,
"to": [
2.7737,
2.7738,
-0.6983
],
"type": "TangentialArc",
@ -176,7 +176,7 @@ description: Variables in memory after executing countersunk-plate.kcl
"sourceRange": []
},
"from": [
2.7737,
2.7738,
-0.6983
],
"tag": null,
@ -196,7 +196,7 @@ description: Variables in memory after executing countersunk-plate.kcl
},
"ccw": false,
"center": [
-0.0,
0.0,
-0.0
],
"from": [

View File

@ -5575,7 +5575,8 @@ description: Artifact commands cpu-cooler.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{
@ -6109,7 +6110,8 @@ description: Artifact commands cpu-cooler.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{
@ -9466,7 +9468,8 @@ description: Artifact commands cpu-cooler.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{
@ -9597,7 +9600,8 @@ description: Artifact commands cpu-cooler.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{
@ -10115,7 +10119,8 @@ description: Artifact commands cpu-cooler.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{
@ -10246,7 +10251,8 @@ description: Artifact commands cpu-cooler.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{

View File

@ -1597,7 +1597,8 @@ description: Artifact commands exhaust-manifold.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{
@ -1608,7 +1609,8 @@ description: Artifact commands exhaust-manifold.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{
@ -1619,7 +1621,8 @@ description: Artifact commands exhaust-manifold.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{
@ -1630,7 +1633,8 @@ description: Artifact commands exhaust-manifold.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{

View File

@ -4490,7 +4490,8 @@ description: Artifact commands utility-sink.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{

View File

@ -5,7 +5,7 @@ description: Error from executing panic_repro_cube.kcl
KCL Semantic error
× semantic: This function expected the input argument to be tag identifier
│ but it's actually of type Unique ID (uuid)
│ but it's actually of type tag
╭─[43:25]
42 │ // these double wrapped functions are the point of this test
43 │ getNextAdjacentEdge(getNextAdjacentEdge(seg01)),

View File

@ -417,7 +417,8 @@ description: Artifact commands subtract_regression03.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{

View File

@ -394,7 +394,8 @@ description: Artifact commands subtract_regression05.kcl
"target": "[uuid]",
"trajectory": "[uuid]",
"sectional": false,
"tolerance": 0.0000001
"tolerance": 0.0000001,
"relative_to": "trajectory_curve"
}
},
{

View File

@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import toast from 'react-hot-toast'
import { useHotkeys } from 'react-hotkeys-hook'
import ModalContainer from 'react-modal-promise'
@ -42,6 +42,7 @@ import {
ONBOARDING_TOAST_ID,
TutorialRequestToast,
} from '@src/routes/Onboarding/utils'
import { reportRejection } from '@src/lib/trap'
// CYCLIC REF
sceneInfra.camControls.engineStreamActor = engineStreamActor
@ -52,6 +53,7 @@ maybeWriteToDisk()
export function App() {
const { project, file } = useLoaderData() as IndexLoaderData
const [nativeFileMenuCreated, setNativeFileMenuCreated] = useState(false)
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
useCreateFileLinkQuery((argDefaultValues) => {
@ -145,12 +147,25 @@ export function App() {
}
}, [location, settings.app.onboardingStatus, navigate])
// Only create the native file menus on desktop
useEffect(() => {
if (isDesktop()) {
window.electron
.createModelingPageMenu()
.then(() => {
setNativeFileMenuCreated(true)
})
.catch(reportRejection)
}
}, [])
return (
<div className="relative h-full flex flex-col" ref={ref}>
<AppHeader
className="transition-opacity transition-duration-75"
project={{ project, file }}
enableMenu={true}
nativeFileMenuCreated={nativeFileMenuCreated}
>
<CommandBarOpenButton />
<ShareButton />

View File

@ -14,6 +14,7 @@ interface AppHeaderProps extends React.PropsWithChildren {
className?: string
enableMenu?: boolean
style?: React.CSSProperties
nativeFileMenuCreated: boolean
}
export const AppHeader = ({
@ -23,12 +24,14 @@ export const AppHeader = ({
className = '',
style,
enableMenu = false,
nativeFileMenuCreated,
}: AppHeaderProps) => {
const user = useUser()
return (
<header
id="app-header"
data-testid="app-header"
className={
'w-full grid ' +
styles.header +
@ -37,6 +40,7 @@ export const AppHeader = ({
}overlaid-panes sticky top-0 z-20 px-2 items-start ` +
className
}
data-native-file-menu={nativeFileMenuCreated}
style={style}
>
<ProjectSidebarMenu

View File

@ -169,6 +169,7 @@ export const CommandBar = () => {
)}
<div className="flex flex-col gap-2 !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent">
<button
data-testid="command-bar-close-button"
onClick={() => commandBarActor.send({ type: 'Close' })}
className="group m-0 p-0 border-none bg-transparent hover:bg-transparent"
>

View File

@ -29,7 +29,7 @@ import { kclCommands } from '@src/lib/kclCommands'
import { BROWSER_PATH, PATHS } from '@src/lib/paths'
import { markOnce } from '@src/lib/performance'
import { codeManager, kclManager } from '@src/lib/singletons'
import { err, reportRejection } from '@src/lib/trap'
import { err } from '@src/lib/trap'
import { type IndexLoaderData } from '@src/lib/types'
import { useSettings, useToken } from '@src/lib/singletons'
import { commandBarActor } from '@src/lib/singletons'
@ -59,12 +59,6 @@ export const FileMachineProvider = ({
const { project, file } = projectData
const filePath = useAbsoluteFilePath()
// Only create the native file menus on desktop
useEffect(() => {
if (isDesktop()) {
window.electron.createModelingPageMenu().catch(reportRejection)
}
}, [])
useEffect(() => {
const {

View File

@ -147,6 +147,14 @@ export class KclManager {
set switchedFiles(switchedFiles: boolean) {
this._switchedFiles = switchedFiles
// These belonged to the previous file
this.lastSuccessfulOperations = []
this.lastSuccessfulVariables = {}
// Without this, when leaving a project which has errors and opening another project which doesn't,
// you'd see the errors from the previous project for a short time until the new code is executed.
this._errors = []
}
get variables() {

View File

@ -31,7 +31,7 @@ const save_ = async (file: ModelingAppFile, toastId: string) => {
)
toast.success(EXPORT_TOAST_MESSAGES.SUCCESS + ' [TEST]', {
id: toastId,
duration: 5_000,
duration: 10_000,
})
return
}

View File

@ -139,18 +139,20 @@ export const settingsMachine = setup({
return () => darkModeMatcher?.removeEventListener('change', listener)
}),
registerCommands: fromCallback<
{ type: 'update' },
{ type: 'update'; settings: SettingsType },
{ settings: SettingsType; actor: AnyActorRef }
>(({ input, receive, system }) => {
// This assumes this actor is running in a system with a command palette
const commandBarActor = system.get(ACTOR_IDS.COMMAND_BAR)
// If the user wants to hide the settings commands
//from the command bar don't add them.
if (settings.commandBar.includeSettings.current === false) return
if (settings.commandBar.includeSettings.current === false) {
return
}
let commands: Command[] = []
const updateCommands = () =>
settingsWithCommandConfigs(input.settings)
const updateCommands = (newSettings: SettingsType) =>
settingsWithCommandConfigs(newSettings)
.map((type) =>
createSettingsCommand({
type,
@ -175,14 +177,19 @@ export const settingsMachine = setup({
data: { commands: commands },
})
receive((event) => {
if (event.type !== 'update') return
receive(({ type, settings: newSettings }) => {
if (type !== 'update') {
return
}
removeCommands()
commands = updateCommands()
commands =
newSettings.commandBar.includeSettings.current === false
? []
: updateCommands(newSettings)
addCommands()
})
commands = updateCommands()
commands = updateCommands(settings)
addCommands()
return () => {
@ -205,7 +212,9 @@ export const settingsMachine = setup({
const sceneInfra = rootContext.sceneInfra
const sceneEntitiesManager = rootContext.sceneEntitiesManager
if (!sceneInfra || !sceneEntitiesManager) return
if (!sceneInfra || !sceneEntitiesManager) {
return
}
const opposingTheme = getOppositeTheme(context.app.theme.current)
sceneInfra.theme = opposingTheme
sceneEntitiesManager.updateSegmentBaseColor(opposingTheme)
@ -213,13 +222,17 @@ export const settingsMachine = setup({
setAllowOrbitInSketchMode: ({ context, self }) => {
const rootContext = self.system.get('root').getSnapshot().context
const sceneInfra = rootContext.sceneInfra
if (!sceneInfra.camControls) return
if (!sceneInfra.camControls) {
return
}
sceneInfra.camControls._setting_allowOrbitInSketchMode =
context.app.allowOrbitInSketchMode.current
// ModelingMachineProvider will do a use effect to trigger the camera engine sync
},
toastSuccess: ({ event }) => {
if (!('data' in event)) return
if (!('data' in event)) {
return
}
const eventParts = event.type.replace(/^set./, '').split('.') as [
keyof typeof settings,
string,
@ -435,6 +448,22 @@ export const settingsMachine = setup({
actions: ['setSettingAtLevel', 'setThemeColor'],
},
'set.commandBar.includeSettings': {
target: 'persisting settings',
actions: [
'setSettingAtLevel',
'toastSuccess',
sendTo(
'registerCommands',
({ context: { currentProject: _, ...settings } }) => ({
type: 'update',
settings,
})
),
],
},
'set.modeling.defaultUnit': {
target: 'persisting settings',
@ -497,6 +526,13 @@ export const settingsMachine = setup({
'setClientTheme',
'setAllowOrbitInSketchMode',
'sendThemeToWatcher',
sendTo(
'registerCommands',
({ context: { currentProject: _, ...settings } }) => ({
type: 'update',
settings,
})
),
],
},
@ -510,6 +546,13 @@ export const settingsMachine = setup({
'setClientTheme',
'setAllowOrbitInSketchMode',
'sendThemeToWatcher',
sendTo(
'registerCommands',
({ context: { currentProject: _, ...settings } }) => ({
type: 'update',
settings,
})
),
],
},
@ -529,7 +572,13 @@ export const settingsMachine = setup({
'clearProjectSettings',
'clearCurrentProject',
'setThemeColor',
sendTo('registerCommands', { type: 'update' }),
sendTo(
'registerCommands',
({ context: { currentProject: _, ...settings } }) => ({
type: 'update',
settings,
})
),
],
},
},
@ -582,6 +631,13 @@ export const settingsMachine = setup({
'setClientTheme',
'setAllowOrbitInSketchMode',
'sendThemeToWatcher',
sendTo(
'registerCommands',
({ context: { currentProject: _, ...settings } }) => ({
type: 'update',
settings,
})
),
],
},
onError: {
@ -612,7 +668,13 @@ export const settingsMachine = setup({
'setClientTheme',
'setAllowOrbitInSketchMode',
'sendThemeToWatcher',
sendTo('registerCommands', { type: 'update' }),
sendTo(
'registerCommands',
({ context: { currentProject: _, ...settings } }) => ({
type: 'update',
settings,
})
),
],
},
onError: 'idle',

View File

@ -1,5 +1,5 @@
import type { FormEvent, HTMLProps } from 'react'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useHotkeys } from 'react-hotkeys-hook'
import {
@ -70,13 +70,20 @@ type ReadWriteProjectState = {
// This route only opens in the desktop context for now,
// as defined in Router.tsx, so we can use the desktop APIs and types.
const Home = () => {
const navigate = useNavigate()
const readWriteProjectDir = useCanReadWriteProjectDirectory()
const [nativeFileMenuCreated, setNativeFileMenuCreated] = useState(false)
const apiToken = useToken()
// Only create the native file menus on desktop
useEffect(() => {
if (isDesktop()) {
window.electron.createHomePageMenu().catch(reportRejection)
window.electron
.createHomePageMenu()
.then(() => {
setNativeFileMenuCreated(true)
})
.catch(reportRejection)
}
billingActor.send({ type: BillingTransition.Update, apiToken })
}, [])
@ -94,7 +101,6 @@ const Home = () => {
})
const location = useLocation()
const navigate = useNavigate()
const settings = useSettings()
const onboardingStatus = settings.app.onboardingStatus.current
@ -217,7 +223,10 @@ const Home = () => {
return (
<div className="relative flex flex-col items-stretch h-screen w-screen overflow-hidden">
<AppHeader showToolbar={false} />
<AppHeader
nativeFileMenuCreated={nativeFileMenuCreated}
showToolbar={false}
/>
<div className="overflow-hidden self-stretch w-full flex-1 home-layout max-w-4xl lg:max-w-5xl xl:max-w-7xl mb-12 px-4 mx-auto mt-8 lg:mt-24 lg:px-0">
<HomeHeader
setQuery={setQuery}

View File

@ -1,6 +1,5 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment, useEffect, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { CustomIcon } from '@src/components/CustomIcon'
@ -10,14 +9,19 @@ import { KeybindingsSectionsList } from '@src/components/Settings/KeybindingsSec
import { SettingsSearchBar } from '@src/components/Settings/SettingsSearchBar'
import { SettingsSectionsList } from '@src/components/Settings/SettingsSectionsList'
import { SettingsTabs } from '@src/components/Settings/SettingsTabs'
import { useDotDotSlash } from '@src/hooks/useDotDotSlash'
import { PATHS } from '@src/lib/paths'
import type { SettingsLevel } from '@src/lib/settings/settingsTypes'
export const Settings = () => {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const close = () => navigate(location.pathname.replace(PATHS.SETTINGS, ''))
const close = () => {
// This makes sure input texts are saved before closing the dialog (eg. default project name).
if (document.activeElement instanceof HTMLInputElement) {
document.activeElement.blur()
}
navigate(location.pathname.replace(PATHS.SETTINGS, ''))
}
const location = useLocation()
const isFileSettings = location.pathname.includes(PATHS.FILE)
const searchParamTab =
@ -25,20 +29,21 @@ export const Settings = () => {
(isFileSettings ? 'project' : 'user')
const scrollRef = useRef<HTMLDivElement>(null)
const dotDotSlash = useDotDotSlash()
useHotkeys('esc', () => navigate(dotDotSlash()))
// Scroll to the hash on load if it exists
useEffect(() => {
console.log('hash', location.hash)
if (location.hash) {
const element = document.getElementById(location.hash.slice(1))
if (element) {
element.scrollIntoView({ block: 'center', behavior: 'smooth' })
;(
element.querySelector('input, select, textarea') as HTMLInputElement
)?.focus()
}
setTimeout(() => {
// GOTCHA: Next tick required, you can instantly navigate to a path and this code will find a null element and not scroll into view.
const element = document.getElementById(location.hash.slice(1))
if (element) {
element.scrollIntoView({ block: 'center', behavior: 'smooth' })
;(
element.querySelector('input, select, textarea') as HTMLInputElement
)?.focus()
}
}, 0)
}
}, [location.hash])