Skip to content

Commit 6dc2e6b

Browse files
committed
Add webstudio workspace creation feature
1 parent 6252964 commit 6dc2e6b

13 files changed

Lines changed: 654 additions & 2 deletions

docs/architecture_distribution_workspace.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ The workspace is user-facing.
6565

6666
The execution mode is implementation-facing.
6767

68+
## Workspace Creation
69+
70+
WebStudio can create a new workspace by cloning an existing workspace.
71+
72+
The base workspace is required.
73+
74+
The new workspace copy includes configuration assets:
75+
76+
- `test.settings`
77+
- `composition.properties`
78+
- `policies.properties`
79+
- Java services, capabilities, and policies
80+
81+
Copying Test Goals is optional and enabled by default.
82+
83+
The objective is a cloned executable workspace while avoiding copied historical execution artifacts.
84+
6885
## CLI Startup Contracts
6986

7087
TESTAR CLI supports two startup contracts.

docs/webstudio/WEBSTUDIO_FUNCTIONAL_SPEC.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,54 @@ The `Edit Settings` view must render `CliStateProjectionMode` as a dropdown usin
123123
- If no `webdriver_xxx` workspace is available, the first workspace is selected.
124124
- Changing the selected workspace reloads the workspace document and resets editor state.
125125

126+
### Workspace Creation
127+
128+
WebStudio must allow creating a new workspace by cloning an existing workspace.
129+
130+
The creation action is launched from the workspace selector area.
131+
132+
The creation dialog must require:
133+
134+
- new workspace name
135+
- base workspace
136+
137+
The base workspace must be one of the existing workspaces.
138+
139+
The new workspace name must be a safe folder name.
140+
141+
Allowed characters:
142+
143+
- letters
144+
- numbers
145+
- underscore
146+
- hyphen
147+
148+
The new workspace name must not already exist.
149+
150+
The cloned workspace must copy:
151+
152+
- `test.settings`
153+
- `composition.properties`
154+
- `policies.properties`
155+
- Java services, capabilities, and policies
156+
157+
The dialog must include an option to copy Test Goals.
158+
159+
Copy Test Goals behavior:
160+
161+
- enabled by default
162+
- if enabled, copy the base workspace `test_goals` folder
163+
- if disabled, create an empty `test_goals` folder for the new workspace
164+
165+
After successful creation:
166+
167+
- WebStudio refreshes the workspace list
168+
- WebStudio selects the new workspace
169+
- WebStudio opens Test Configuration
170+
- the default configuration editor is `Edit Java Composition Flow`
171+
172+
If creation fails, WebStudio must keep the creation dialog open and show the error.
173+
126174
### Page Navigation
127175

128176
Main pages:

docs/webstudio/WEBSTUDIO_UX_SPEC.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,25 @@ Examples:
7575

7676
- runtime pages may show `SUTConnectorValue`, but editing remains in configuration
7777

78+
## Workspace Selector UX Contract
79+
80+
The workspace selector area is the primary place for workspace selection and creation.
81+
82+
Expected behavior:
83+
84+
- an `Add` action to the left of the workspace selector opens a create-workspace modal
85+
- the modal asks whether the user wants to create a new workspace
86+
- the modal requires a new workspace name
87+
- the modal requires selecting an existing workspace as the base
88+
- `Copy Test Goals` is visible, checked by default, and can be unchecked
89+
- `Create` remains disabled until the workspace name and base workspace are valid
90+
- `Discard` closes the modal without creating anything
91+
- after creation, the new workspace is selected automatically
92+
93+
The create-workspace modal must not resize the page layout.
94+
95+
Validation feedback appears inside the modal.
96+
7897
## Viewport and Scroll Contract
7998

8099
Expected desktop behavior:

webstudio/frontend/src/App.svelte

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
import { stateModelWorkspaceDialog } from "./stateModelNavigation.js";
1515
import { objectSnapshot } from "./editorDirtyState.js";
1616
import { resultFileUrl, resultGroupDeleteUrl, resultListUrl } from "./resultApi.js";
17+
import {
18+
defaultWorkspaceCreationDraft,
19+
workspaceCreationRequest,
20+
workspaceCreationValidation
21+
} from "./workspaceCreationModel.js";
1722
1823
const STATE_MODEL_URL = "http://localhost:8090/models";
1924
const CLI_AGENT_SETTING_KEYS = {
@@ -95,6 +100,9 @@
95100
message: "",
96101
saveLabel: "Save"
97102
};
103+
let workspaceCreationDialogOpen = false;
104+
let workspaceCreationDraft = defaultWorkspaceCreationDraft([], "");
105+
let workspaceCreationError = "";
98106
let savedTestSettingsContent = "";
99107
let savedCompositionPropertiesContent = "";
100108
let savedPoliciesPropertiesContent = "";
@@ -809,6 +817,62 @@
809817
});
810818
}
811819
820+
async function openWorkspaceCreationDialog() {
821+
await guardApplicationTransition(async () => {
822+
workspaceCreationDraft = defaultWorkspaceCreationDraft(workspaces, selectedWorkspaceName);
823+
workspaceCreationError = "";
824+
workspaceCreationDialogOpen = true;
825+
});
826+
}
827+
828+
function closeWorkspaceCreationDialog() {
829+
if (saving) {
830+
return;
831+
}
832+
833+
workspaceCreationDialogOpen = false;
834+
workspaceCreationError = "";
835+
}
836+
837+
function closeWorkspaceCreationDialogFromBackdrop(event) {
838+
if (event.target === event.currentTarget) {
839+
closeWorkspaceCreationDialog();
840+
}
841+
}
842+
843+
async function createWorkspaceFromDialog() {
844+
const validation = workspaceCreationValidation(workspaceCreationDraft, workspaces);
845+
if (!validation.valid) {
846+
workspaceCreationError = validation.message;
847+
return;
848+
}
849+
850+
saving = true;
851+
workspaceCreationError = "";
852+
message = "";
853+
854+
try {
855+
const request = workspaceCreationRequest(workspaceCreationDraft);
856+
const createdWorkspace = await loadJson("/api/workspaces", {
857+
method: "POST",
858+
headers: {
859+
"Content-Type": "application/json"
860+
},
861+
body: JSON.stringify(request)
862+
});
863+
await refreshInitialData();
864+
currentPage = "configuration";
865+
await loadWorkspace(createdWorkspace?.name || request.name);
866+
workspaceCreationDialogOpen = false;
867+
showTemporaryMessage(`Workspace ${request.name} created.`);
868+
} catch (createError) {
869+
reportClientError("Unable to create workspace", createError);
870+
workspaceCreationError = createError.message || "Unable to create workspace.";
871+
} finally {
872+
saving = false;
873+
}
874+
}
875+
812876
function openStateModelExternalTab(url = STATE_MODEL_URL) {
813877
window.open(url, "_blank", "noopener,noreferrer");
814878
}
@@ -2170,6 +2234,7 @@
21702234
}
21712235
21722236
$: selectedWorkspaceSummary = workspaces.find((workspace) => workspace.name === selectedWorkspaceName) || null;
2237+
$: workspaceCreationValidationState = workspaceCreationValidation(workspaceCreationDraft, workspaces);
21732238
21742239
onMount(async () => {
21752240
startScriptlessPolling();
@@ -2196,6 +2261,9 @@
21962261
<div class="page">
21972262
<nav class="panel panel-wide page-nav">
21982263
<div class="page-nav-workspace">
2264+
<button type="button" class="secondary page-workspace-add" on:click={openWorkspaceCreationDialog}>
2265+
New
2266+
</button>
21992267
<select
22002268
id="page-workspace-select"
22012269
value={selectedWorkspaceName}
@@ -2382,6 +2450,68 @@
23822450
</div>
23832451
{/if}
23842452
2453+
{#if workspaceCreationDialogOpen}
2454+
<div class="composition-modal-backdrop" role="presentation" on:click={closeWorkspaceCreationDialogFromBackdrop}>
2455+
<div
2456+
class="composition-modal state-model-dialog workspace-creation-dialog"
2457+
role="dialog"
2458+
aria-modal="true"
2459+
aria-labelledby="workspace-creation-dialog-title"
2460+
>
2461+
<div class="composition-modal-header">
2462+
<div>
2463+
<h2 id="workspace-creation-dialog-title">Create New Workspace</h2>
2464+
<p>Select an existing workspace as base. WebStudio clones its settings, composition, policies, and Java files.</p>
2465+
</div>
2466+
</div>
2467+
<div class="composition-modal-body workspace-creation-form">
2468+
<label>
2469+
<span>Workspace name</span>
2470+
<input
2471+
type="text"
2472+
bind:value={workspaceCreationDraft.name}
2473+
placeholder="platform_application (e.g., webdriver_parabank)"
2474+
disabled={saving}
2475+
/>
2476+
</label>
2477+
<label>
2478+
<span>Base workspace</span>
2479+
<select bind:value={workspaceCreationDraft.baseWorkspace} disabled={saving}>
2480+
{#each workspaces as workspace}
2481+
<option value={workspace.name}>{workspace.name}</option>
2482+
{/each}
2483+
</select>
2484+
</label>
2485+
<label class="workspace-creation-checkbox">
2486+
<input
2487+
type="checkbox"
2488+
bind:checked={workspaceCreationDraft.copyTestGoals}
2489+
disabled={saving}
2490+
/>
2491+
<span>Copy Test Goals from base workspace</span>
2492+
</label>
2493+
{#if workspaceCreationError || workspaceCreationValidationState.message}
2494+
<p class:settings-validation-invalid={workspaceCreationError || !workspaceCreationValidationState.valid}>
2495+
{workspaceCreationError || workspaceCreationValidationState.message}
2496+
</p>
2497+
{/if}
2498+
</div>
2499+
<div class="composition-modal-actions">
2500+
<button
2501+
type="button"
2502+
on:click={createWorkspaceFromDialog}
2503+
disabled={saving || !workspaceCreationValidationState.valid}
2504+
>
2505+
Create
2506+
</button>
2507+
<button type="button" class="secondary" on:click={closeWorkspaceCreationDialog} disabled={saving}>
2508+
Discard
2509+
</button>
2510+
</div>
2511+
</div>
2512+
</div>
2513+
{/if}
2514+
23852515
{#if stateModelDialog.open}
23862516
<div class="composition-modal-backdrop" role="presentation" on:click={closeStateModelDialogFromBackdrop}>
23872517
<div

webstudio/frontend/src/styles/layout.css

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@
5555
}
5656

5757
.page-nav-workspace {
58-
display: grid;
59-
gap: 6px;
58+
display: flex;
59+
gap: 8px;
60+
align-items: center;
6061
min-width: 220px;
6162
}
6263

@@ -72,6 +73,11 @@
7273
min-width: 220px;
7374
}
7475

76+
.page-workspace-add {
77+
min-height: 39px;
78+
white-space: nowrap;
79+
}
80+
7581
.page-nav-right-group {
7682
margin-left: auto;
7783
display: flex;

webstudio/frontend/src/styles/test-configuration.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,41 @@
994994
margin-top: 16px;
995995
}
996996

997+
.workspace-creation-dialog {
998+
width: min(720px, 100%);
999+
}
1000+
1001+
.workspace-creation-form {
1002+
display: grid;
1003+
gap: 14px;
1004+
}
1005+
1006+
.workspace-creation-form label {
1007+
display: grid;
1008+
gap: 6px;
1009+
color: var(--text);
1010+
font-weight: 700;
1011+
}
1012+
1013+
.workspace-creation-form label span {
1014+
font-size: 0.88rem;
1015+
}
1016+
1017+
.workspace-creation-checkbox {
1018+
display: flex !important;
1019+
grid-template-columns: none;
1020+
align-items: center;
1021+
gap: 10px !important;
1022+
padding: 10px 12px;
1023+
border-radius: 12px;
1024+
background: rgba(235, 238, 242, 0.72);
1025+
border: 1px solid rgba(35, 52, 79, 0.1);
1026+
}
1027+
1028+
.workspace-creation-checkbox input {
1029+
width: auto;
1030+
}
1031+
9971032
.composition-modal-body textarea {
9981033
min-height: 600px;
9991034
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const WORKSPACE_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
2+
3+
export function defaultWorkspaceCreationDraft(workspaces, selectedWorkspaceName) {
4+
return {
5+
name: "",
6+
baseWorkspace: selectedWorkspaceName || workspaces?.[0]?.name || "",
7+
copyTestGoals: true
8+
};
9+
}
10+
11+
export function workspaceCreationValidation(draft, workspaces) {
12+
const workspaceName = (draft?.name || "").trim();
13+
const baseWorkspace = (draft?.baseWorkspace || "").trim();
14+
const existingNames = new Set((workspaces || []).map((workspace) => workspace.name));
15+
16+
if (!workspaceName) {
17+
return {
18+
valid: false,
19+
message: "Enter a workspace name."
20+
};
21+
}
22+
23+
if (!WORKSPACE_NAME_PATTERN.test(workspaceName)) {
24+
return {
25+
valid: false,
26+
message: "Use only letters, numbers, underscores, and hyphens."
27+
};
28+
}
29+
30+
if (existingNames.has(workspaceName)) {
31+
return {
32+
valid: false,
33+
message: "A workspace with this name already exists."
34+
};
35+
}
36+
37+
if (!baseWorkspace || !existingNames.has(baseWorkspace)) {
38+
return {
39+
valid: false,
40+
message: "Select an existing base workspace."
41+
};
42+
}
43+
44+
return {
45+
valid: true,
46+
message: ""
47+
};
48+
}
49+
50+
export function workspaceCreationRequest(draft) {
51+
return {
52+
name: (draft?.name || "").trim(),
53+
baseWorkspace: (draft?.baseWorkspace || "").trim(),
54+
copyTestGoals: draft?.copyTestGoals !== false
55+
};
56+
}

0 commit comments

Comments
 (0)