Desktop: Add File > Save As… (#3034)

* Make file name and document name identical

* Add save as action

* Fix test errors

* Add missing save as action

* Desktop fix drop file open document file message

* Address review comments

* Replace file save suffix with file extension

* Add comment specifying that the upload function takes a html input accept string

* Fix remove file extension in web

* Use let

* Don't show save as menu entry in web

* Don't add SaveDocumentAs in web

* Remove file extension on all open document file calls

---------

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Timon
2025-08-20 10:09:01 +00:00
committed by GitHub
co-authored by Dennis Kobert
parent 7c30f6168b
commit e70862b399
19 changed files with 152 additions and 73 deletions
@@ -151,9 +151,11 @@
return;
}
if (file.name.endsWith(".graphite")) {
const graphiteFileSuffix = "." + editor.handle.fileExtension();
if (file.name.endsWith(graphiteFileSuffix)) {
const content = await file.text();
editor.handle.openDocumentFile(file.name, content);
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
editor.handle.openDocumentFile(documentName, content);
return;
}
});
+4 -2
View File
@@ -438,9 +438,11 @@
}
// When we eventually have sub-documents, this should be changed to import the document instead of opening it in a separate tab
if (file.name.endsWith(".graphite")) {
const graphiteFileSuffix = "." + editor.handle.fileExtension();
if (file.name.endsWith(graphiteFileSuffix)) {
const content = await file.text();
editor.handle.openDocumentFile(file.name, content);
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
editor.handle.openDocumentFile(documentName, content);
return;
}
});
@@ -83,9 +83,11 @@
return;
}
if (file.name.endsWith(".graphite")) {
const graphiteFileSuffix = "." + editor.handle.fileExtension();
if (file.name.endsWith(graphiteFileSuffix)) {
const content = await file.text();
editor.handle.openDocumentFile(file.name, content);
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
editor.handle.openDocumentFile(documentName, content);
return;
}
});
+5 -2
View File
@@ -334,8 +334,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height);
}
if (file.name.endsWith(".graphite")) {
editor.handle.openDocumentFile(file.name, await file.text());
const graphiteFileSuffix = "." + editor.handle.fileExtension();
if (file.name.endsWith(graphiteFileSuffix)) {
const content = await file.text();
const documentName = file.name.slice(0, -graphiteFileSuffix.length);
editor.handle.openDocumentFile(documentName, content);
}
});
}
+14 -5
View File
@@ -62,9 +62,16 @@ export function createPortfolioState(editor: Editor) {
}
});
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
const extension = editor.handle.fileSaveSuffix();
const data = await upload(extension, "text");
editor.handle.openDocumentFile(data.filename, data.content);
const suffix = "." + editor.handle.fileExtension();
const data = await upload(suffix, "text");
// Use filename as document name, removing the extension if it exists
let documentName = data.filename;
if (documentName.endsWith(suffix)) {
documentName = documentName.slice(0, -suffix.length);
}
editor.handle.openDocumentFile(documentName, data.content);
});
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
const data = await upload("image/*", "both");
@@ -76,8 +83,10 @@ export function createPortfolioState(editor: Editor) {
}
// In case the user accidentally uploads a Graphite file, open it instead of failing to import it
if (data.filename.endsWith(".graphite")) {
editor.handle.openDocumentFile(data.filename, data.content.text);
const graphiteFileSuffix = "." + editor.handle.fileExtension();
if (data.filename.endsWith(graphiteFileSuffix)) {
const documentName = data.filename.slice(0, -graphiteFileSuffix.length);
editor.handle.openDocumentFile(documentName, data.content.text);
return;
}
+3 -2
View File
@@ -22,11 +22,12 @@ export function downloadFile(filename: string, content: ArrayBuffer) {
downloadFileBlob(filename, blob);
}
export async function upload<T extends "text" | "data" | "both">(acceptedExtensions: string, textOrData: T): Promise<UploadResult<T>> {
// See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#accept for the `accept` string format
export async function upload<T extends "text" | "data" | "both">(accept: string, textOrData: T): Promise<UploadResult<T>> {
return new Promise<UploadResult<T>>((resolve, _) => {
const element = document.createElement("input");
element.type = "file";
element.accept = acceptedExtensions;
element.accept = accept;
element.addEventListener(
"change",
+9 -7
View File
@@ -6,7 +6,7 @@
//
use crate::helpers::translate_key;
use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER};
use editor::consts::FILE_SAVE_SUFFIX;
use editor::consts::FILE_EXTENSION;
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@@ -344,10 +344,10 @@ impl EditorHandle {
cfg!(debug_assertions)
}
/// Get the constant `FILE_SAVE_SUFFIX`
#[wasm_bindgen(js_name = fileSaveSuffix)]
pub fn file_save_suffix(&self) -> String {
FILE_SAVE_SUFFIX.into()
/// Get the constant `FILE_EXTENSION`
#[wasm_bindgen(js_name = fileExtension)]
pub fn file_extension(&self) -> String {
FILE_EXTENSION.into()
}
/// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that)
@@ -421,7 +421,8 @@ impl EditorHandle {
#[wasm_bindgen(js_name = openDocumentFile)]
pub fn open_document_file(&self, document_name: String, document_serialized_content: String) {
let message = PortfolioMessage::OpenDocumentFile {
document_name,
document_name: Some(document_name),
document_path: None,
document_serialized_content,
};
self.dispatch(message);
@@ -432,7 +433,8 @@ impl EditorHandle {
let document_id = DocumentId(document_id);
let message = PortfolioMessage::OpenDocumentFileWithId {
document_id,
document_name,
document_name: Some(document_name),
document_path: None,
document_is_auto_saved: true,
document_is_saved,
document_serialized_content,