Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -1479,6 +1479,7 @@ packages/lib/models/Alarm.js
packages/lib/models/BaseItem.test.js
packages/lib/models/BaseItem.js
packages/lib/models/ConflictNoteState.js
packages/lib/models/Folder.publishing.test.js
packages/lib/models/Folder.sharing.test.js
packages/lib/models/Folder.test.js
packages/lib/models/Folder.js
Expand Down
1 change: 1 addition & 0 deletions .ignore.eslint
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,7 @@ packages/lib/models/Alarm.js
packages/lib/models/BaseItem.test.js
packages/lib/models/BaseItem.js
packages/lib/models/ConflictNoteState.js
packages/lib/models/Folder.publishing.test.js
packages/lib/models/Folder.sharing.test.js
packages/lib/models/Folder.test.js
packages/lib/models/Folder.js
Expand Down
11 changes: 11 additions & 0 deletions packages/lib/Synchronizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,17 @@ export default class Synchronizer {
}
}

try {
// Update published/unpublished status after the main sync to avoid conflicts.
// See https://github.com/laurent22/joplin/issues/16167.
if (!hasCaughtError && !this.cancelling()) {
await Note.updatePublishedNotes(this.shareService_ ? this.shareService_.shares : []);
}
} catch (error) {
logger.error('Failed to save note publication status', error);
this.progressReport_.errors.push(error);
}

if (syncLock) {
this.lockHandler().stopAutoLockRefresh(syncLock);
await this.lockHandler().releaseLock(LockType.Sync, this.lockClientType(), this.clientId_);
Expand Down
104 changes: 104 additions & 0 deletions packages/lib/models/Folder.publishing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { setupDatabaseAndSynchronizer, switchClient, resourceService, createFolderTree } from '../testing/test-utils';
import Folder from '../models/Folder';
import { ShareType, StateShare } from '../services/share/reducer';
import Note from './Note';

const publishedFolderShareState = (folderId: string): StateShare => ({
id: `share-${folderId}`,
type: ShareType.PublishedFolder,
folder_id: folderId,
note_id: '',
master_key_id: '',
});

type ItemSlice = { title: string };

const expectPublished = async (items: ItemSlice[], published = true) => {
for (let item of items) {
item = (await Folder.loadByTitle(item.title)) ?? await Note.loadByTitle(item.title);
expect(item).toMatchObject({
title: item.title,
is_shared: published ? 1 : 0,
});
}
};

const expectUnpublished = (items: ItemSlice[]) => expectPublished(items, false);

describe('models/Folder.publishing', () => {

beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
});

it('should set is_shared=1 on descendants of a published folder', async () => {
const root = await createFolderTree('', [
{
title: 'root',
children: [
{
title: 'sub-folder 1',
children: [],
},
{
title: 'sub-folder 2',
children: [
{ title: 'sub-sub-folder', children: [] },
{ title: 'published note 1' },
{ title: 'published note 2' },
],
},
{ title: 'published note 3' },
],
},
{
title: 'unpublished folder',
children: [
{ title: 'unpublished note' },
],
},
]);

const shareState: StateShare[] = [
publishedFolderShareState(root.id),
];

await Folder.updateAllShareIds(
resourceService(),
shareState,
);

// After the first round, only folders should have is_shared=1
await expectPublished([
'root',
'sub-folder 1',
'sub-folder 2',
'sub-sub-folder',
].map(title => ({ title })));
await expectUnpublished([
'published note 1',
'published note 2',
'published note 3',
'unpublished folder',
'unpublished note',
].map(title => ({ title })));

// Should update published notes when calling Note.updateNotePublicationStatus
await Note.updatePublishedNotes(shareState);

await expectPublished([
'root',
'sub-folder 1',
'sub-folder 2',
'sub-sub-folder',
'published note 1',
'published note 2',
'published note 3',
].map(title => ({ title })));
await expectUnpublished([
'unpublished folder',
'unpublished note',
].map(title => ({ title })));
});
});
38 changes: 10 additions & 28 deletions packages/lib/models/Folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -781,49 +781,31 @@ export default class Folder extends BaseItem {
public static async updateAllShareIds(resourceService: ResourceService, activeShares: StateShare[]) {
await this.updateFolderShareIds(activeShares);
await this.updateNoteShareIds();
await this.updateResourceShareIds(resourceService);

// Don't update note publication status here: Doing so can cause conflicts if updateAllShareIds
// is called just before sync
await this.updateFolderPublishStatus_(activeShares);
}

private static async updateFolderPublishStatus_(activeShares: StateShare[]) {
const publishedFolderRootIds = activeShares
.filter(share => share.type === ShareType.PublishedFolder && !!share.folder_id)
.map(share => share.folder_id);
const directlyPublishedNoteIds = activeShares
.filter(share => share.type === ShareType.Note && !!share.note_id)
.map(share => share.note_id);
const publishedFolderIds = unique(publishedFolderRootIds.concat(
...(await Promise.all(publishedFolderRootIds.map(id => this.allChildrenFolders(id)))).map(folders => folders.map(f => f.id)),
(await Promise.all(
publishedFolderRootIds.map(id => this.allChildrenFolders(id)),
)).flatMap(folders => folders.map(f => f.id)),
));

const publishedFolderIdSet = new Set(publishedFolderIds);
const directlyPublishedNoteIdSet = new Set(directlyPublishedNoteIds);

if (publishedFolderIds.length) {
for (const folder of await this.all({ fields: ['id', 'is_shared'] })) {
if (!publishedFolderIdSet.has(folder.id)) continue;
await this.updateShareStatus({ ...folder, type_: BaseModel.TYPE_FOLDER }, true);
}
}

const noteIsSharedSql = [
directlyPublishedNoteIds.length ? `id IN (${this.escapeIdsForSql(directlyPublishedNoteIds)})` : '',
publishedFolderIds.length ? `parent_id IN (${this.escapeIdsForSql(publishedFolderIds)})` : '',
].filter(v => !!v).join(' OR ');

let notesToUpdate: NoteEntity[] = [];
if (noteIsSharedSql) {
notesToUpdate = await this.db().selectAll(`
SELECT id, parent_id, is_shared
FROM notes
WHERE is_shared = 0 AND (${noteIsSharedSql})
`);
}

for (const note of notesToUpdate) {
await this.updateShareStatus(
{ ...note, type_: BaseModel.TYPE_NOTE },
directlyPublishedNoteIdSet.has(note.id) || publishedFolderIdSet.has(note.parent_id),
);
}

await this.updateResourceShareIds(resourceService);
}

// Clear the "share_id" property for the items that are associated with a
Expand Down
33 changes: 33 additions & 0 deletions packages/lib/models/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ALL_NOTES_FILTER_ID } from '../reserved-ids';
import NoteLockNote from '../services/noteLock/NoteLockNote';
import isNoteLockEnabled from '../services/noteLock/isNoteLockEnabled';
import isItemId from './utils/isItemId';
import { ShareType, StateShare } from '../services/share/reducer';

export interface PreviewsOrder {
by: string;
Expand Down Expand Up @@ -599,6 +600,38 @@ export default class Note extends BaseItem {
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0');
}

public static async updatePublishedNotes(activeShares: StateShare[]) {
const directlyPublishedNoteIds = activeShares
.filter(share => share.type === ShareType.Note && !!share.note_id)
.map(share => share.note_id);

const loadUnpublishedWithDirectShare = async (): Promise<NoteEntity[]> => {
if (directlyPublishedNoteIds.length === 0) return [];

return await this.db().selectAll(`
SELECT id, parent_id, is_shared
FROM notes
WHERE is_shared = 0 AND id IN (${this.escapeIdsForSql(directlyPublishedNoteIds)})
`);
};
const unpublishedNotesInPublishedFolders: NoteEntity[] = await this.db().selectAll(`
SELECT notes.id, notes.parent_id, notes.is_shared
FROM notes
JOIN folders ON notes.parent_id = folders.id
WHERE notes.is_shared = 0 AND folders.is_shared = 1
AND notes.is_conflict = 0
AND notes.deleted_time = 0
`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const notesToPublish = unpublishedNotesInPublishedFolders.concat(await loadUnpublishedWithDirectShare());
for (const note of notesToPublish) {
await this.updateShareStatus(
{ ...note, type_: BaseModel.TYPE_NOTE },
true,
);
}
}

public static async updateGeolocation(noteId: string): Promise<NoteEntity | null> {
if (!Setting.value('trackLocation')) return null;
if (!Note.updateGeolocationEnabled_) return null;
Expand Down
Loading