Edit a text file where you are already reading it
v2 of the viewer: Edit, on text and Markdown, in the dialog and on the row menu. Save is explicit -- every save mints a new blob, so autosave would burn quota and multiply the conflicts it cannot see. Two people editing one file is the case worth getting right. `saveText` re-reads the node and compares the blob the editor started from: if somebody else saved in the meantime it refuses, says so, and leaves the work in the box to copy out. `ifInState` is the obvious tool and the wrong one -- it is the state of every FileNode in the account, so an unrelated upload in another folder would fail the save, and a warning that cries wolf is a warning people click through. Editing is not offered where saving would lose something: a file truncated for display would have its tail written away, and one that did not decode as UTF-8 would have mojibake written over whatever encoding it really is. Both open read-only and say which. Nor is it offered without mayModifyContent -- a read-only share just has no Edit. Closing or cancelling with unsaved changes asks first, Ctrl+S saves, and mail attachments are unaffected: they pass no onSave, because a message part is not a thing that can be written back.
This commit is contained in:
@@ -5,6 +5,7 @@ import { foldersNeeded, type PlannedUpload } from "@/lib/dropUpload";
|
||||
import { isAppFolder } from "@/lib/appFolder";
|
||||
import type { FileNode, GetResponse, Id, QueryResponse, SetResponse } from "@/jmap/types";
|
||||
import { useSession } from "./session";
|
||||
import { t as translate } from "@/lib/i18n";
|
||||
|
||||
interface SharedAccount {
|
||||
id: Id;
|
||||
@@ -56,6 +57,12 @@ interface FilesState {
|
||||
mkdir(parentId: Id | null, name: string): Promise<Id>;
|
||||
upload(parentId: Id | null, files: File[]): Promise<void>;
|
||||
rename(id: Id, name: string): Promise<void>;
|
||||
/**
|
||||
* Write text back over a file. `seenBlobId` is what the editor started from:
|
||||
* if the node has moved on since, somebody else saved and this throws rather
|
||||
* than quietly winning.
|
||||
*/
|
||||
saveText(id: Id, text: string, seenBlobId: Id | null): Promise<Id>;
|
||||
move(id: Id, parentId: Id | null): Promise<void>;
|
||||
destroy(ids: Id[]): Promise<void>;
|
||||
refresh(ids: Id[]): Promise<void>;
|
||||
@@ -307,6 +314,35 @@ export const useFiles = create<FilesState>((set, get) => ({
|
||||
void get().loadTree();
|
||||
},
|
||||
|
||||
async saveText(id, text, seenBlobId) {
|
||||
const accountId = get().accountId!;
|
||||
/*
|
||||
* Look before writing.
|
||||
*
|
||||
* `ifInState` is the obvious tool and the wrong one here: it is the state
|
||||
* of every FileNode in the account, so an unrelated upload in another
|
||||
* folder would fail this save, and a reader who is told "someone changed
|
||||
* it" when nobody did learns to click through the warning. The node's own
|
||||
* blobId is the thing that actually answers the question.
|
||||
*/
|
||||
const fresh = await client.call<GetResponse<FileNode>>("FileNode/get", { accountId, ids: [id], properties: fileNodeProps() });
|
||||
const now = fresh.list[0];
|
||||
if (!now) throw new Error(translate("That file is no longer there."));
|
||||
if (now.blobId !== seenBlobId) throw new Error(translate("Somebody else saved this file while it was open. Copy your changes, close it, and start again."));
|
||||
|
||||
const type = now.type || "text/plain";
|
||||
const blob = new Blob([text], { type });
|
||||
const up = await client.upload(accountId, blob, { type });
|
||||
const res = await client.call<SetResponse<FileNode>>("FileNode/set", {
|
||||
accountId,
|
||||
update: { [id]: { blobId: up.blobId, type, size: blob.size } },
|
||||
});
|
||||
const err = res.notUpdated?.[id];
|
||||
if (err) throw new Error(setErrorMessage(err));
|
||||
await get().refresh([id]);
|
||||
return up.blobId;
|
||||
},
|
||||
|
||||
async rename(id, name) {
|
||||
const accountId = get().accountId!;
|
||||
const res = await client.call<SetResponse>("FileNode/set", { accountId, update: { [id]: { name } } });
|
||||
|
||||
Reference in New Issue
Block a user