Skip to content

Commit

Permalink
add logic to create file
Browse files Browse the repository at this point in the history
  • Loading branch information
madeindjs committed Jan 10, 2025
1 parent dd3d23e commit 2506b85
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 26 deletions.
9 changes: 8 additions & 1 deletion src/ui/src/builder/panels/BuilderCodePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
:source-files="sourceFileDraft as SourceFilesDirectory"
:path-active="filepathOpen"
:paths-unsaved="pathsUnsaved"
display-add-file-button
class="BuilderCodePanel__content__tree"
@add-file="handleAddFile"
@select="openFile"
/>
</template>
Expand All @@ -39,7 +41,7 @@
</template>

<script setup lang="ts">
import { computed, defineAsyncComponent, inject, ref, toRaw, watch } from "vue";
import { computed, defineAsyncComponent, inject, ref, watch } from "vue";
import BuilderPanel, { type BuilderPanelAction } from "./BuilderPanel.vue";
import BuilderAsyncLoader from "../BuilderAsyncLoader.vue";
import injectionKeys from "@/injectionKeys";
Expand Down Expand Up @@ -73,6 +75,10 @@ function onOpenPanel(open: boolean) {
if (filepathOpen.value === undefined) openFile(["main.py"]);
}
function handleAddFile() {
wf.sendCreateSourceFileRequest(["it-works.txt"]);
}
const status = ref<null | {
type: "error" | "success" | "neutral";
message: string;
Expand Down Expand Up @@ -120,6 +126,7 @@ async function save() {
isDisabled.value = true;
try {
await wf.sendCodeSaveRequest(code.value, filepathOpen.value);
isDisabled.value = false;
} catch {
status.value = {
type: "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ const props = defineProps({
path: { type: Array as PropType<string[]>, default: () => [] },
pathActive: { type: Array as PropType<string[]>, default: () => [] },
pathsUnsaved: { type: Array as PropType<string[][]>, default: () => [] },
displayAddFileButton: { type: Boolean },
});
defineEmits({
select: (path: string[]) => Array.isArray(path),
addFile: () => true,
});
const sourceFilesEntries = computed(() =>
Expand All @@ -36,6 +38,13 @@ const sourceFilesEntries = computed(() =>
:paths-unsaved="pathsUnsaved"
@select="$emit('select', $event)"
/>
<button
v-if="displayAddFileButton"
class="SharedSourceFilesTree__addFile"
@click="$emit('addFile')"
>
Add file
</button>
</div>
</template>

Expand All @@ -44,4 +53,13 @@ const sourceFilesEntries = computed(() =>
display: flex;
flex-direction: column;
}
.SharedSourceFilesTree__addFile {
margin-left: 32px;
text-align: left;
border: none;
background-color: transparent;
height: 32px;
color: var(--wdsColorBlue5);
cursor: pointer;
}
</style>
48 changes: 31 additions & 17 deletions src/ui/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ import { parseAccessor } from "./parsing";
import { loadExtensions } from "./loadExtensions";
import { bigIntReplacer } from "./serializer";
import { useLogger } from "@/composables/useLogger";
import { findSourceFileFromPath, isSourceFilesFile } from "./sourceFiles";
import {
createFileToSourceFiles,
findSourceFileFromPath,
isSourceFilesFile,
} from "./sourceFiles";
import { createSourceFile } from "typescript";

const RECONNECT_DELAY_MS = 1000;
const KEEP_ALIVE_DELAY_MS = 60000;
Expand Down Expand Up @@ -353,10 +358,7 @@ export function generateCore() {
ok: boolean;
payload?: Record<string, any>;
}) => {
if (!r.ok) {
reject("Couldn't connect to the server.");
return;
}
if (!r.ok) return reject("Couldn't connect to the server.");
resolve(r.payload?.message);
};

Expand All @@ -374,6 +376,25 @@ export function generateCore() {
}
}

async function sendCreateSourceFileRequest(path: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const messageCallback = (r: {
ok: boolean;
payload?: Record<string, any>;
}) => {
if (!r.ok) return reject("Couldn't connect to the server.");

sourceFiles.value = createFileToSourceFiles(
path,
toRaw(sourceFiles.value),
);
resolve();
};

sendFrontendMessage("createSourceFile", { path }, messageCallback);
});
}

async function sendCodeSaveRequest(
code: string,
path = ["main.py"],
Expand All @@ -383,10 +404,8 @@ export function generateCore() {
ok: boolean;
payload?: Record<string, any>;
}) => {
if (!r.ok) {
reject("Couldn't connect to the server.");
return;
}
if (!r.ok) return reject("Couldn't connect to the server.");

updateSourceFile(code, path);
resolve();
};
Expand All @@ -408,10 +427,7 @@ export function generateCore() {
ok: boolean;
payload?: { content: string };
}) => {
if (!r.ok) {
reject("Couldn't connect to the server.");
return;
}
if (!r.ok) return reject("Couldn't connect to the server.");

updateSourceFile(r.payload.content, path);

Expand Down Expand Up @@ -521,10 +537,7 @@ export function generateCore() {
ok: boolean;
payload?: Record<string, any>;
}) => {
if (!r.ok) {
reject("Couldn't connect to the server.");
return;
}
if (!r.ok) return reject("Couldn't connect to the server.");
resolve();
};
sendFrontendMessage("componentUpdate", payload, messageCallback);
Expand Down Expand Up @@ -617,6 +630,7 @@ export function generateCore() {
runCode: readonly(runCode),
sourceFiles: readonly(sourceFiles),
sendCodeSaveRequest,
sendCreateSourceFileRequest,
requestSourceFileLoading,
sendComponentUpdate,
addComponent,
Expand Down
31 changes: 31 additions & 0 deletions src/ui/src/core/sourceFiles.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect } from "vitest";

import {
createFileToSourceFiles,
findSourceFileFromPath,
getSourceFilesPathsToFiles,
} from "./sourceFiles";
Expand Down Expand Up @@ -95,3 +96,33 @@ describe(getSourceFilesPathsToFiles.name, () => {
]);
});
});

describe(createFileToSourceFiles.name, () => {
it("should create the file", () => {
const result = createFileToSourceFiles(["a", "b", "c.txt"], {
type: "directory",
children: {},
});

expect(result).toStrictEqual({
type: "directory",
children: {
a: {
type: "directory",
children: {
b: {
type: "directory",
children: {
"c.txt": {
type: "file",
content: "",
complete: true,
},
},
},
},
},
},
});
});
});
26 changes: 26 additions & 0 deletions src/ui/src/core/sourceFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,29 @@ export function findSourceFileFromPath(
? node
: findSourceFileFromPath(restPath, node);
}

export function createFileToSourceFiles(path: string[], tree: SourceFiles) {
const copy = structuredClone(tree);

const newFile: SourceFilesFile = {
type: "file",
content: "",
complete: true,
};
let node = copy;

while (path.length > 0) {
const key = path.shift();

if (!isSourceFilesDirectory(node)) throw Error();

if (path.length === 0) {
node.children[key] = newFile;
} else {
node.children[key] ??= { type: "directory", children: {} };
node = node.children[key];
}
}

return copy;
}
20 changes: 18 additions & 2 deletions src/ui/src/core/useSourceFiles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,27 @@ describe(useSourceFiles.name, () => {
expect(pathsUnsaved.value).toStrictEqual([["a.txt"], ["c", "c.txt"]]);
});

it("should add a file to the draft", () => {
const { core, sourceFiles } = buildMockCore();
sourceFiles.value = { type: "directory", children: {} };

const { sourceFileDraft } = useSourceFiles(core);
expect(sourceFileDraft.value).toStrictEqual(sourceFileDraft.value);

sourceFiles.value = {
type: "directory",
children: { "a.txt": { type: "file", content: "" } },
};
expect(sourceFileDraft.value).toStrictEqual(sourceFileDraft.value);
});

describe("lazy loading", () => {
it("should request fetchig whole file on opening", () => {
const { core, sourceFiles } = buildMockCore();

const requestSourceFileLoading = vi
.spyOn(core, "requestSourceFileLoading")
.mockResolvedValue("");
.mockImplementation(async () => {});

sourceFiles.value = {
type: "directory",
Expand Down Expand Up @@ -102,7 +116,9 @@ describe(useSourceFiles.name, () => {
it("should refresh the draft when", async () => {
const { core, sourceFiles } = buildMockCore();

vi.spyOn(core, "requestSourceFileLoading").mockResolvedValue("");
vi.spyOn(core, "requestSourceFileLoading").mockImplementation(
async () => {},
);

sourceFiles.value = {
type: "directory",
Expand Down
23 changes: 18 additions & 5 deletions src/writer/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ def _start_wf_project_process_write_files(self):
wf_project.start_process_write_files_async(self.wf_project_context, AppRunner.WF_PROJECT_SAVE_INTERVAL)

def load(self) -> None:
self.run_code = self._load_persisted_script("main.py")
self.run_code = self.load_persisted_script("main.py")
self.source_files = self._build_file_tree()
self.bmc_components = self._load_persisted_components()

Expand Down Expand Up @@ -701,7 +701,7 @@ def _build_file_tree(self) -> Dict:
current_level = current_level['children'][part]
else:
try:
content = self._load_persisted_script(relative_path)
content = self.load_persisted_script(relative_path)
# limit only the first 100 characters to limit bandwidth usage, the rest will be lazy loaded
experp = content[0:100]
current_level['children'][part] = {
Expand Down Expand Up @@ -749,12 +749,25 @@ async def dispatch_message(self, session_id: Optional[str], request: AppProcessS

return response

def _load_persisted_script(self, file = "main.py") -> str:

def create_persisted_script(self, file = "main.py"):
path = os.path.join(self.app_path, file)

# ensure we don't load a file outside of the application (like `../../../etc/passwd`)
if not os.path.abspath(path).startswith(self.app_path):
raise PermissionError(f"{file} is outside of application ({self.app_path})")

with open(path, "x", encoding='utf-8') as f:
f.write('')

self.source_files = self._build_file_tree()

def load_persisted_script(self, file = "main.py") -> str:
path = os.path.join(self.app_path, file)

# ensure we don't load a file outside of the application (like `../../../etc/passwd`)
if not os.path.abspath(path).startswith(self.app_path):
raise FileNotFoundError(f"{file} is outside of application ({self.app_path})")
raise PermissionError(f"{file} is outside of application ({self.app_path})")

logger = logging.getLogger('writer')
try:
Expand Down Expand Up @@ -923,7 +936,7 @@ def _start_app_process(self) -> None:
def reload_code_from_saved(self) -> None:
if not self.is_app_process_server_ready.is_set():
return
self.update_code(None, self._load_persisted_script())
self.update_code(None, self.load_persisted_script())

def update_code(self, session_id: Optional[str], run_code: str) -> None:

Expand Down
16 changes: 15 additions & 1 deletion src/writer/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,9 @@ async def _stream_incoming_requests(websocket: WebSocket, session_id: str):
elif serve_mode == "edit" and req_message.type == "loadSourceFile":
new_task = asyncio.create_task(
_handle_load_source_file_request(websocket, session_id, req_message))
elif serve_mode == "edit" and req_message.type == "createSourceFile":
new_task = asyncio.create_task(
_handle_create_source_file_request(websocket, session_id, req_message))
elif serve_mode == "edit":
new_task = asyncio.create_task(
_handle_incoming_edit_message(websocket, session_id, req_message))
Expand Down Expand Up @@ -600,7 +603,7 @@ async def _handle_load_source_file_request(websocket: WebSocket, session_id: str
path = os.path.join(*req_message.payload['path'])
content = ""
try:
content = app_runner._load_persisted_script(path)
content = app_runner.load_persisted_script(path)
except FileNotFoundError as error:
logging.warning(f"could not load script at {path}", error)

Expand All @@ -611,6 +614,17 @@ async def _handle_load_source_file_request(websocket: WebSocket, session_id: str
)
await websocket.send_json(response.model_dump())

async def _handle_create_source_file_request(websocket: WebSocket, session_id: str, req_message: WriterWebsocketIncoming):
path = os.path.join(*req_message.payload['path'])
app_runner.create_persisted_script(path)

response = WriterWebsocketOutgoing(
messageType=f"{req_message.type}Response",
trackingId=req_message.trackingId,
payload=None
)
await websocket.send_json(response.model_dump())


async def _stream_outgoing_announcements(websocket: WebSocket):

Expand Down

0 comments on commit 2506b85

Please sign in to comment.