Skip to content

Commit d6f4dc2

Browse files
Add project ids to all projects on open (#7949)
* Add project ids to all projects on open * Fix id generation on web * Add two simple project id tests * make check * cargo fmt * Fix cargo test * Update settings docs * Fix named view test * cargo fmt * Fix named views * Fix test * make check * Update src/lib/settings/initialSettings.tsx Co-authored-by: Frank Noirot <[email protected]> * address potential for bad heuristic * revert --------- Co-authored-by: Frank Noirot <[email protected]>
1 parent c3d1f9c commit d6f4dc2

File tree

10 files changed

+210
-19
lines changed

10 files changed

+210
-19
lines changed

docs/kcl-lang/settings/project.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ base_unit = "in"
3131

3232

3333

34+
#### meta
35+
36+
Information about the project itself. Choices about how settings are merged have prevent me (lee) from easily moving this out of the settings structure.
37+
38+
39+
**Default:** None
40+
41+
This setting has the following nested options:
42+
43+
##### id
44+
45+
46+
47+
48+
**Default:** None
49+
50+
3451
#### app
3552

3653
The settings for the Design Studio.

e2e/playwright/fixtures/toolbarFixture.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export class ToolbarFixture {
5050
loadButton!: Locator
5151
/** User button for the user sidebar menu */
5252
userSidebarButton!: Locator
53+
/** Project button for the project's settings */
54+
projectSidebarToggle!: Locator
5355
signOutButton!: Locator
5456
/** Selection indicator text in the status bar */
5557
selectionStatus!: Locator
@@ -93,6 +95,7 @@ export class ToolbarFixture {
9395
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
9496

9597
this.userSidebarButton = page.getByTestId('user-sidebar-toggle')
98+
this.projectSidebarToggle = page.getByTestId('project-sidebar-toggle')
9699
this.signOutButton = page.getByTestId('user-sidebar-sign-out')
97100

98101
this.selectionStatus = page.getByTestId('selection-status')

e2e/playwright/named-views.spec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ nameToUuid.set('uuid3', uuid3)
3939
* Given the project.toml string, overwrite the named views to be the constant uuid
4040
* values to match the snapshots. The uuids are randomly generated
4141
*/
42-
function tomlStringOverWriteNamedViewUuids(toml: string): string {
42+
function tomlStringMakeTestDataNotAsFragile(toml: string): string {
4343
const settings = tomlToPerProjectSettings(toml)
44+
delete settings.settings?.meta
4445
const namedViews = settings.settings?.app?.named_views
4546
if (namedViews) {
4647
const entries = Object.entries(namedViews)
@@ -120,7 +121,7 @@ test.describe('Named view tests', () => {
120121
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
121122

122123
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
123-
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
124+
tomlString = tomlStringMakeTestDataNotAsFragile(tomlString)
124125

125126
// Write the entire tomlString to a snapshot.
126127
// There are many key/value pairs to check this is a safer match.
@@ -166,7 +167,7 @@ test.describe('Named view tests', () => {
166167
// Read project.toml into memory
167168
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
168169
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
169-
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
170+
tomlString = tomlStringMakeTestDataNotAsFragile(tomlString)
170171

171172
// Write the entire tomlString to a snapshot.
172173
// There are many key/value pairs to check this is a safer match.
@@ -186,7 +187,7 @@ test.describe('Named view tests', () => {
186187
// Read project.toml into memory again since we deleted a named view
187188
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
188189
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
189-
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
190+
tomlString = tomlStringMakeTestDataNotAsFragile(tomlString)
190191

191192
// Write the entire tomlString to a snapshot.
192193
// There are many key/value pairs to check this is a safer match.
@@ -232,7 +233,7 @@ test.describe('Named view tests', () => {
232233
// Read project.toml into memory
233234
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
234235
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
235-
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
236+
tomlString = tomlStringMakeTestDataNotAsFragile(tomlString)
236237

237238
// Write the entire tomlString to a snapshot.
238239
// There are many key/value pairs to check this is a safer match.
@@ -301,7 +302,7 @@ test.describe('Named view tests', () => {
301302
// Read project.toml into memory
302303
let tomlString = await fsp.readFile(tempProjectSettingsFilePath, 'utf-8')
303304
// Rewrite the uuids in the named views to match snapshot otherwise they will be randomly generated from rust and break
304-
tomlString = tomlStringOverWriteNamedViewUuids(tomlString)
305+
tomlString = tomlStringMakeTestDataNotAsFragile(tomlString)
305306

306307
// Write the entire tomlString to a snapshot.
307308
// There are many key/value pairs to check this is a safer match.

e2e/playwright/projects.spec.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'fs'
2+
import { NIL as uuidNIL } from 'uuid'
23
import path from 'path'
3-
import { DEFAULT_PROJECT_KCL_FILE } from '@src/lib/constants'
4+
import { DEFAULT_PROJECT_KCL_FILE, REGEXP_UUIDV4 } from '@src/lib/constants'
45
import fsp from 'fs/promises'
56

67
import {
@@ -1799,3 +1800,68 @@ profile001 = startProfile(sketch001, at = [0, 0])
17991800
})
18001801
}
18011802
)
1803+
1804+
test.describe('Project id', () => {
1805+
// Should work on both web and desktop.
1806+
test(
1807+
'is created on new project',
1808+
{
1809+
tag: ['@desktop', '@web'],
1810+
},
1811+
async ({ page, toolbar, context, homePage }, testInfo) => {
1812+
const u = await getUtils(page)
1813+
await page.setBodyDimensions({ width: 1200, height: 500 })
1814+
await homePage.goToModelingScene()
1815+
await u.waitForPageLoad()
1816+
1817+
const inputProjectId = page.getByTestId('project-id')
1818+
1819+
await test.step('Open the project settings modal', async () => {
1820+
await toolbar.projectSidebarToggle.click()
1821+
await page.getByTestId('project-settings').click()
1822+
// Give time to system for writing to a persistent store
1823+
await page.waitForTimeout(1000)
1824+
})
1825+
1826+
await test.step('Check project id is not the NIL UUID and not empty', async () => {
1827+
await expect(inputProjectId).not.toHaveValue(uuidNIL)
1828+
await expect(inputProjectId).toHaveValue(REGEXP_UUIDV4)
1829+
})
1830+
}
1831+
)
1832+
test(
1833+
'is created on existing project without one',
1834+
{ tag: '@desktop' },
1835+
async ({ page, toolbar, context, homePage }, testInfo) => {
1836+
const u = await getUtils(page)
1837+
await context.folderSetupFn(async (rootDir) => {
1838+
const projectDir = path.join(rootDir, 'hoohee')
1839+
await fsp.mkdir(projectDir, { recursive: true })
1840+
await fsp.writeFile(
1841+
path.join(projectDir, 'project.toml'),
1842+
`[settings.app]
1843+
themeColor = "255"
1844+
`
1845+
)
1846+
})
1847+
1848+
await page.setBodyDimensions({ width: 1200, height: 500 })
1849+
await homePage.goToModelingScene()
1850+
await u.waitForPageLoad()
1851+
1852+
const inputProjectId = page.getByTestId('project-id')
1853+
1854+
await test.step('Open the project settings modal', async () => {
1855+
await toolbar.projectSidebarToggle.click()
1856+
await page.getByTestId('project-settings').click()
1857+
// Give time to system for writing to a persistent store
1858+
await page.waitForTimeout(1000)
1859+
})
1860+
1861+
await test.step('Check project id is not the NIL UUID and not empty', async () => {
1862+
await expect(inputProjectId).not.toHaveValue(uuidNIL)
1863+
await expect(inputProjectId).toHaveValue(REGEXP_UUIDV4)
1864+
})
1865+
}
1866+
)
1867+
})

rust/kcl-lib/src/settings/types/project.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ impl ProjectConfiguration {
4141
#[ts(export)]
4242
#[serde(rename_all = "snake_case")]
4343
pub struct PerProjectSettings {
44+
/// Information about the project itself.
45+
/// Choices about how settings are merged have prevent me (lee) from easily
46+
/// moving this out of the settings structure.
47+
#[serde(default)]
48+
#[validate(nested)]
49+
pub meta: ProjectMetaSettings,
50+
4451
/// The settings for the Design Studio.
4552
#[serde(default)]
4653
#[validate(nested)]
@@ -59,6 +66,15 @@ pub struct PerProjectSettings {
5966
pub command_bar: CommandBarSettings,
6067
}
6168

69+
/// Information about the project.
70+
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq, Validate)]
71+
#[ts(export)]
72+
#[serde(rename_all = "snake_case")]
73+
pub struct ProjectMetaSettings {
74+
#[serde(default, skip_serializing_if = "is_default")]
75+
pub id: uuid::Uuid,
76+
}
77+
6278
/// Project specific application settings.
6379
// TODO: When we remove backwards compatibility with the old settings file, we can remove the
6480
// aliases to camelCase (and projects plural) from everywhere.
@@ -167,7 +183,7 @@ mod tests {
167183

168184
use super::{
169185
CommandBarSettings, NamedView, PerProjectSettings, ProjectAppSettings, ProjectAppearanceSettings,
170-
ProjectConfiguration, ProjectModelingSettings, TextEditorSettings,
186+
ProjectConfiguration, ProjectMetaSettings, ProjectModelingSettings, TextEditorSettings,
171187
};
172188
use crate::settings::types::UnitLength;
173189

@@ -182,7 +198,9 @@ mod tests {
182198
let serialized = toml::to_string(&parsed).unwrap();
183199
assert_eq!(
184200
serialized,
185-
r#"[settings.app]
201+
r#"[settings.meta]
202+
203+
[settings.app]
186204
187205
[settings.modeling]
188206
@@ -268,6 +286,7 @@ color = 1567.4"#;
268286
fn test_project_settings_named_views() {
269287
let conf = ProjectConfiguration {
270288
settings: PerProjectSettings {
289+
meta: ProjectMetaSettings { id: uuid::Uuid::nil() },
271290
app: ProjectAppSettings {
272291
appearance: ProjectAppearanceSettings { color: 138.0.into() },
273292
onboarding_status: Default::default(),
@@ -323,7 +342,9 @@ color = 1567.4"#;
323342
},
324343
};
325344
let serialized = toml::to_string(&conf).unwrap();
326-
let old_project_file = r#"[settings.app]
345+
let old_project_file = r#"[settings.meta]
346+
347+
[settings.app]
327348
show_debug_panel = true
328349
329350
[settings.app.appearance]

src/components/ProjectSidebarMenu.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ function ProjectMenuPopover({
123123
Element: 'button',
124124
children: (
125125
<>
126-
<span className="flex-1">Project settings</span>
126+
<span className="flex-1" data-testid="project-settings">
127+
Project settings
128+
</span>
127129
<kbd className="hotkey">{`${platform === 'macos' ? '⌘' : 'Ctrl'}${
128130
isDesktop() ? '' : '⬆'
129131
},`}</kbd>

src/lib/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,6 @@ export type EnvironmentConfigurationRuntime = {
248248
export const ENVIRONMENT_CONFIGURATION_FOLDER = 'envs'
249249

250250
export const MAX_PROJECT_NAME_LENGTH = 240
251+
252+
// It's so ugh that `uuid` package doesn't export this.
253+
export const REGEXP_UUIDV4 = /^[0-9A-F]{8}(-[0-9A-F]{4}){3}-[0-9A-F]{12}$/i

src/lib/routeLoaders.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,25 @@ export const fileLoader: LoaderFunction = async (
3535
routerData
3636
): Promise<FileLoaderData | Response> => {
3737
const { params } = routerData
38-
let { configuration } = await loadAndValidateSettings()
38+
39+
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
40+
41+
const heuristicProjectFilePath =
42+
isDesktop() && params.id
43+
? params.id
44+
.split(window.electron.sep)
45+
.slice(0, -1)
46+
.join(window.electron.sep)
47+
: undefined
48+
49+
let settings = await loadAndValidateSettings(heuristicProjectFilePath)
3950

4051
const projectPathData = await getProjectMetaByRouteId(
4152
readAppSettingsFile,
4253
readLocalStorageAppSettingsFile,
4354
params.id,
44-
configuration
55+
settings.configuration
4556
)
46-
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
47-
4857
let code = ''
4958

5059
if (!isBrowserProject && projectPathData) {

src/lib/settings/initialSettings.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjecti
55
import type { NamedView } from '@rust/kcl-lib/bindings/NamedView'
66
import type { OnboardingStatus } from '@rust/kcl-lib/bindings/OnboardingStatus'
77

8+
import { NIL as uuidNIL } from 'uuid'
9+
810
import { CustomIcon } from '@src/components/CustomIcon'
911
import Tooltip from '@src/components/Tooltip'
1012
import type { CameraSystem } from '@src/lib/cameraControls'
1113
import { cameraMouseDragGuards, cameraSystems } from '@src/lib/cameraControls'
1214
import {
1315
DEFAULT_DEFAULT_LENGTH_UNIT,
1416
DEFAULT_PROJECT_NAME,
17+
REGEXP_UUIDV4,
1518
} from '@src/lib/constants'
1619
import { isDesktop } from '@src/lib/isDesktop'
1720
import type {
@@ -127,9 +130,6 @@ const MS_IN_MINUTE = 1000 * 60
127130

128131
export function createSettings() {
129132
return {
130-
/** Settings that affect the behavior of the entire app,
131-
* beyond just modeling or navigating, for example
132-
*/
133133
app: {
134134
/**
135135
* The overall appearance of the app: light, dark, or system
@@ -653,6 +653,32 @@ export function createSettings() {
653653
},
654654
}),
655655
},
656+
/** Settings that affect the behavior of the entire app,
657+
* beyond just modeling or navigating, for example
658+
* NOTE: before using the project id for anything, check it isn't the
659+
* NIL uuid, which is the default value.
660+
*/
661+
meta: {
662+
id: new Setting<string>({
663+
hideOnLevel: 'user',
664+
defaultValue: uuidNIL,
665+
description: 'The unique project identifier',
666+
// Never allow the user to change the id, only view it.
667+
validate: (v) => REGEXP_UUIDV4.test(v),
668+
Component: ({ value }) => {
669+
return (
670+
<div className="flex gap-4 p-1 border rounded-sm border-chalkboard-30">
671+
<input
672+
className="flex-grow text-xs px-2 bg-chalkboard-30 dark:bg-chalkboard-80 cursor-not-allowed"
673+
value={value}
674+
disabled
675+
data-testid="project-id"
676+
/>
677+
</div>
678+
)
679+
},
680+
}),
681+
},
656682
}
657683
}
658684

0 commit comments

Comments
 (0)