fix: files show creation date instead of modification date #700
This commit is contained in:
@@ -1046,7 +1046,7 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
const node: FileNode = {
|
const node: FileNode = {
|
||||||
id: generateDemoId('file'),
|
id: generateDemoId('file'),
|
||||||
parentId, name, type: 'd', blobId: null, size: 0,
|
parentId, name, type: 'd', blobId: null, size: 0,
|
||||||
created: new Date().toISOString(), updated: new Date().toISOString(),
|
created: new Date().toISOString(), modified: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
this.data.fileNodes.push(node);
|
this.data.fileNodes.push(node);
|
||||||
return node;
|
return node;
|
||||||
@@ -1056,7 +1056,7 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
const node: FileNode = {
|
const node: FileNode = {
|
||||||
id: generateDemoId('file'),
|
id: generateDemoId('file'),
|
||||||
parentId, name, type, blobId, size,
|
parentId, name, type, blobId, size,
|
||||||
created: new Date().toISOString(), updated: new Date().toISOString(),
|
created: new Date().toISOString(), modified: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
this.data.fileNodes.push(node);
|
this.data.fileNodes.push(node);
|
||||||
return node;
|
return node;
|
||||||
@@ -1064,7 +1064,7 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
|
|
||||||
async updateFileNode(id: string, updates: Partial<Pick<FileNode, 'name' | 'parentId'>>): Promise<void> {
|
async updateFileNode(id: string, updates: Partial<Pick<FileNode, 'name' | 'parentId'>>): Promise<void> {
|
||||||
const node = this.data.fileNodes.find(n => n.id === id);
|
const node = this.data.fileNodes.find(n => n.id === id);
|
||||||
if (node) Object.assign(node, updates, { updated: new Date().toISOString() });
|
if (node) Object.assign(node, updates, { modified: new Date().toISOString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFileNodes(updates: Record<string, Partial<Pick<FileNode, 'name' | 'parentId'>>>): Promise<{ updated: string[]; notUpdated: Record<string, string> }> {
|
async updateFileNodes(updates: Record<string, Partial<Pick<FileNode, 'name' | 'parentId'>>>): Promise<{ updated: string[]; notUpdated: Record<string, string> }> {
|
||||||
@@ -1072,7 +1072,7 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
for (const [id, patch] of Object.entries(updates)) {
|
for (const [id, patch] of Object.entries(updates)) {
|
||||||
const node = this.data.fileNodes.find(n => n.id === id);
|
const node = this.data.fileNodes.find(n => n.id === id);
|
||||||
if (node) {
|
if (node) {
|
||||||
Object.assign(node, patch, { updated: new Date().toISOString() });
|
Object.assign(node, patch, { modified: new Date().toISOString() });
|
||||||
updated.push(id);
|
updated.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function createDemoFileNodes(): FileNode[] {
|
|||||||
blobId: null,
|
blobId: null,
|
||||||
size: 0,
|
size: 0,
|
||||||
created: demoDate(-30),
|
created: demoDate(-30),
|
||||||
updated: demoDate(-2),
|
modified: demoDate(-2),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'demo-file-photos',
|
id: 'demo-file-photos',
|
||||||
@@ -22,7 +22,7 @@ export function createDemoFileNodes(): FileNode[] {
|
|||||||
blobId: null,
|
blobId: null,
|
||||||
size: 0,
|
size: 0,
|
||||||
created: demoDate(-30),
|
created: demoDate(-30),
|
||||||
updated: demoDate(-5),
|
modified: demoDate(-5),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Documents contents
|
// Documents contents
|
||||||
@@ -34,7 +34,7 @@ export function createDemoFileNodes(): FileNode[] {
|
|||||||
blobId: 'demo-blob-file-1',
|
blobId: 'demo-blob-file-1',
|
||||||
size: 2150,
|
size: 2150,
|
||||||
created: demoDate(-7),
|
created: demoDate(-7),
|
||||||
updated: demoDate(-2),
|
modified: demoDate(-2),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'demo-file-quarterly-report',
|
id: 'demo-file-quarterly-report',
|
||||||
@@ -44,7 +44,7 @@ export function createDemoFileNodes(): FileNode[] {
|
|||||||
blobId: 'demo-blob-file-2',
|
blobId: 'demo-blob-file-2',
|
||||||
size: 148480,
|
size: 148480,
|
||||||
created: demoDate(-14),
|
created: demoDate(-14),
|
||||||
updated: demoDate(-14),
|
modified: demoDate(-14),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'demo-file-todo',
|
id: 'demo-file-todo',
|
||||||
@@ -54,7 +54,7 @@ export function createDemoFileNodes(): FileNode[] {
|
|||||||
blobId: 'demo-blob-file-3',
|
blobId: 'demo-blob-file-3',
|
||||||
size: 410,
|
size: 410,
|
||||||
created: demoDate(-3),
|
created: demoDate(-3),
|
||||||
updated: demoDate(-1),
|
modified: demoDate(-1),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Photos contents
|
// Photos contents
|
||||||
@@ -66,7 +66,7 @@ export function createDemoFileNodes(): FileNode[] {
|
|||||||
blobId: 'demo-blob-file-4',
|
blobId: 'demo-blob-file-4',
|
||||||
size: 1258291,
|
size: 1258291,
|
||||||
created: demoDate(-10),
|
created: demoDate(-10),
|
||||||
updated: demoDate(-10),
|
modified: demoDate(-10),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'demo-file-team-photo',
|
id: 'demo-file-team-photo',
|
||||||
@@ -76,7 +76,7 @@ export function createDemoFileNodes(): FileNode[] {
|
|||||||
blobId: 'demo-blob-file-5',
|
blobId: 'demo-blob-file-5',
|
||||||
size: 911360,
|
size: 911360,
|
||||||
created: demoDate(-21),
|
created: demoDate(-21),
|
||||||
updated: demoDate(-21),
|
modified: demoDate(-21),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Root-level file
|
// Root-level file
|
||||||
@@ -88,7 +88,7 @@ export function createDemoFileNodes(): FileNode[] {
|
|||||||
blobId: 'demo-blob-file-6',
|
blobId: 'demo-blob-file-6',
|
||||||
size: 68608,
|
size: 68608,
|
||||||
created: demoDate(-5),
|
created: demoDate(-5),
|
||||||
updated: demoDate(-1),
|
modified: demoDate(-1),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -5534,7 +5534,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static FILE_NODE_PROPERTIES = [
|
private static FILE_NODE_PROPERTIES = [
|
||||||
"id", "parentId", "name", "type", "blobId", "size", "created", "updated",
|
"id", "parentId", "name", "type", "blobId", "size", "created", "modified",
|
||||||
// Stalwart omits shareWith/myRights from FileNode/get unless requested
|
// Stalwart omits shareWith/myRights from FileNode/get unless requested
|
||||||
// explicitly, so the share dialog and indicators can't see existing
|
// explicitly, so the share dialog and indicators can't see existing
|
||||||
// shares without naming them here (same as CALENDAR_PROPERTIES).
|
// shares without naming them here (same as CALENDAR_PROPERTIES).
|
||||||
|
|||||||
+5
-1
@@ -835,7 +835,11 @@ export interface FileNode {
|
|||||||
blobId: string | null;
|
blobId: string | null;
|
||||||
size: number;
|
size: number;
|
||||||
created: string;
|
created: string;
|
||||||
updated: string;
|
// Last content/metadata change, server-maintained. The property is named
|
||||||
|
// `modified` in draft-ietf-jmap-filenode and in Stalwart - there is no
|
||||||
|
// `updated` on a FileNode. Asking for the wrong name silently yields
|
||||||
|
// undefined, which made the UI show the creation date forever (#700).
|
||||||
|
modified: string;
|
||||||
// JMAP Sharing (RFC 9670). Populated only when the server advertises the
|
// JMAP Sharing (RFC 9670). Populated only when the server advertises the
|
||||||
// filenode capability and the properties are explicitly requested. A node is
|
// filenode capability and the properties are explicitly requested. A node is
|
||||||
// shared-out when `shareWith` has entries; `myRights` describes what the
|
// shared-out when `shareWith` has entries; `myRights` describes what the
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ function makeMockClient(initial: FileNode[] = []) {
|
|||||||
},
|
},
|
||||||
async createFileDirectory(name: string, parentId: string | null) {
|
async createFileDirectory(name: string, parentId: string | null) {
|
||||||
// A real folder has no content blob (this is how Stalwart marks a container).
|
// A real folder has no content blob (this is how Stalwart marks a container).
|
||||||
const node: FileNode = { id: `n${++seq}`, parentId, name, type: '', blobId: null, size: 0, created: now(), updated: now() };
|
const node: FileNode = { id: `n${++seq}`, parentId, name, type: '', blobId: null, size: 0, created: now(), modified: now() };
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
return { ...node };
|
return { ...node };
|
||||||
},
|
},
|
||||||
async createFileNode(name: string, blobId: string, type: string, size: number, parentId: string | null) {
|
async createFileNode(name: string, blobId: string, type: string, size: number, parentId: string | null) {
|
||||||
const node: FileNode = { id: `n${++seq}`, parentId, name, type, blobId, size, created: now(), updated: now() };
|
const node: FileNode = { id: `n${++seq}`, parentId, name, type, blobId, size, created: now(), modified: now() };
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
return { ...node };
|
return { ...node };
|
||||||
},
|
},
|
||||||
@@ -92,15 +92,15 @@ function makeMockClient(initial: FileNode[] = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dir = (id: string, name: string, parentId: string | null): FileNode => ({
|
const dir = (id: string, name: string, parentId: string | null): FileNode => ({
|
||||||
id, parentId, name, type: 'd', blobId: null, size: 0, created: '', updated: '',
|
id, parentId, name, type: 'd', blobId: null, size: 0, created: '', modified: '',
|
||||||
});
|
});
|
||||||
// An old build's "folder": a directory-typed node that is actually a blob-backed
|
// An old build's "folder": a directory-typed node that is actually a blob-backed
|
||||||
// file, so the server won't let anything be parented under it.
|
// file, so the server won't let anything be parented under it.
|
||||||
const marker = (id: string, name: string, parentId: string | null): FileNode => ({
|
const marker = (id: string, name: string, parentId: string | null): FileNode => ({
|
||||||
id, parentId, name, type: 'd', blobId: `b-${id}`, size: 0, created: '', updated: '',
|
id, parentId, name, type: 'd', blobId: `b-${id}`, size: 0, created: '', modified: '',
|
||||||
});
|
});
|
||||||
const file = (id: string, name: string, parentId: string | null): FileNode => ({
|
const file = (id: string, name: string, parentId: string | null): FileNode => ({
|
||||||
id, parentId, name, type: 'text/plain', blobId: `b-${id}`, size: 10, created: '', updated: '',
|
id, parentId, name, type: 'text/plain', blobId: `b-${id}`, size: 10, created: '', modified: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('file-store hierarchy (issue #379)', () => {
|
describe('file-store hierarchy (issue #379)', () => {
|
||||||
@@ -343,7 +343,7 @@ describe('file-store hierarchy (issue #379)', () => {
|
|||||||
// containers). The migration must detect this and undo its marker rename.
|
// containers). The migration must detect this and undo its marker rename.
|
||||||
(client as unknown as { createFileDirectory: typeof client.createFileDirectory }).createFileDirectory =
|
(client as unknown as { createFileDirectory: typeof client.createFileDirectory }).createFileDirectory =
|
||||||
async (name: string, parentId: string | null) =>
|
async (name: string, parentId: string | null) =>
|
||||||
({ id: 'bad', parentId, name, type: 'd', blobId: 'b-bad', size: 0, created: '', updated: '' });
|
({ id: 'bad', parentId, name, type: 'd', blobId: 'b-bad', size: 0, created: '', modified: '' });
|
||||||
useFileStore.getState().initClient(client);
|
useFileStore.getState().initClient(client);
|
||||||
|
|
||||||
expect(await useFileStore.getState().migrateLegacyFlatNodes()).toBe(false);
|
expect(await useFileStore.getState().migrateLegacyFlatNodes()).toBe(false);
|
||||||
@@ -373,3 +373,41 @@ describe('file-store hierarchy (issue #379)', () => {
|
|||||||
expect(client._nodes()).toHaveLength(0);
|
expect(client._nodes()).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('file-store modification date (issue #700)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useFileStore.setState({
|
||||||
|
client: null,
|
||||||
|
currentParentId: null,
|
||||||
|
currentPath: '/',
|
||||||
|
pathStack: [{ id: null, name: '' }],
|
||||||
|
resources: [],
|
||||||
|
selectedResources: new Set(),
|
||||||
|
clipboard: null,
|
||||||
|
lastAction: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the server-maintained `modified` date, not `created`', async () => {
|
||||||
|
// Stalwart names the property `modified` (draft-ietf-jmap-filenode); there
|
||||||
|
// is no `updated`. Asking for the wrong name used to leave lastModified
|
||||||
|
// pinned to the creation date, so replacing a file looked unmodified.
|
||||||
|
const client = makeMockClient([
|
||||||
|
{ ...file('notes', 'Notes.md', null), created: '2025-01-01T00:00:00Z', modified: '2026-07-30T12:00:00Z' },
|
||||||
|
]);
|
||||||
|
useFileStore.getState().initClient(client);
|
||||||
|
|
||||||
|
await useFileStore.getState().navigate(null);
|
||||||
|
expect(useFileStore.getState().resources[0].lastModified).toBe('2026-07-30T12:00:00Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to `created` when the server sends no `modified`', async () => {
|
||||||
|
const client = makeMockClient([
|
||||||
|
{ ...file('notes', 'Notes.md', null), created: '2025-01-01T00:00:00Z', modified: '' },
|
||||||
|
]);
|
||||||
|
useFileStore.getState().initClient(client);
|
||||||
|
|
||||||
|
await useFileStore.getState().navigate(null);
|
||||||
|
expect(useFileStore.getState().resources[0].lastModified).toBe('2025-01-01T00:00:00Z');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ function nodeToResource(node: FileNode): FileResource {
|
|||||||
isDirectory: isDir,
|
isDirectory: isDir,
|
||||||
contentType: isDir ? '' : node.type,
|
contentType: isDir ? '' : node.type,
|
||||||
contentLength: node.size,
|
contentLength: node.size,
|
||||||
lastModified: node.updated || node.created,
|
lastModified: node.modified || node.created,
|
||||||
blobId: node.blobId,
|
blobId: node.blobId,
|
||||||
parentId: node.parentId,
|
parentId: node.parentId,
|
||||||
myRights: node.myRights,
|
myRights: node.myRights,
|
||||||
|
|||||||
Reference in New Issue
Block a user