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
10 changes: 10 additions & 0 deletions migrations/tenant/0036-note-settings@add-sidebar-position.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Create "sidebar_position" column in note_settings table
DO $$

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could add pg enum type and use it for note_settings field, VARCHAR(255) is too wide

BEGIN
IF NOT EXISTS(SELECT *
FROM information_schema.columns
WHERE table_name='note_settings' and column_name='sidebar_position')
THEN
ALTER TABLE "public"."note_settings" ADD COLUMN "sidebar_position" VARCHAR(255) NOT NULL DEFAULT 'content';
END IF;
END $$;
7 changes: 6 additions & 1 deletion src/domain/entities/NoteHierarchy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NotePublicId } from './note.js';
import type { SidebarPosition } from './noteSettings.js';

/**
* Note Tree entity
Expand All @@ -15,9 +16,13 @@ export interface NoteHierarchy {
*/
noteTitle: string;

/**
* Position of the root note in the sidebar
*/
sidebarPosition?: SidebarPosition;

/**
* child notes
*/
childNotes: NoteHierarchy[] | null;

}
6 changes: 6 additions & 0 deletions src/domain/entities/note.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type User from '@domain/entities/user.js';
import type EditorTool from './editorTools.js';
import type { SidebarPosition } from './noteSettings.js';

/**
* Note internal id. Used to query Note by internal API
Expand Down Expand Up @@ -111,4 +112,9 @@ export type NotePreview = {
* Parent note id
*/
parentId: NoteInternalId | null;

/**
* Position of the note in the sidebar
*/
sidebarPosition: SidebarPosition;
};
15 changes: 13 additions & 2 deletions src/domain/entities/noteSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import type { Team } from './team.js';
*/
export type InvitationHash = string;

/**
* Position of the note in the sidebar
*/
export type SidebarPosition = 'edge' | 'content' | 'none';

/**
* Notes settings entity
*/
Expand Down Expand Up @@ -39,6 +44,11 @@ export default interface NoteSettings {
*/
cover: string;

/**
* Position of the note in the sidebar
*/
sidebarPosition: SidebarPosition;

/**
* Team members. Team is empty by default because note creator is not stored in team
*/
Expand All @@ -48,7 +58,7 @@ export default interface NoteSettings {
/**
* Attributes of public note settings
*/
type NoteSettingsPublicProperties = 'customHostname' | 'isPublic' | 'invitationHash' | 'team' | 'cover' ;
type NoteSettingsPublicProperties = 'customHostname' | 'isPublic' | 'invitationHash' | 'team' | 'cover' | 'sidebarPosition' ;

export interface NoteSettingsPublic extends Pick<NoteSettings, NoteSettingsPublicProperties> {}

Expand All @@ -63,13 +73,14 @@ export function definePublicNoteSettings(noteSettings: NoteSettings): NoteSettin
invitationHash: noteSettings.invitationHash,
team: noteSettings.team,
cover: noteSettings.cover,
sidebarPosition: noteSettings.sidebarPosition,
};
}

/**
* Notes settings creation attributes, omitting id, because it's generated by database, omitting cover, because it should be generated after insertion of the note
*/
type NoteSettingsCreationAttributes = Omit<NoteSettings, 'id' | 'cover'>;
type NoteSettingsCreationAttributes = Omit<NoteSettings, 'id' | 'cover' | 'sidebarPosition'>;

export type {
NoteSettingsCreationAttributes
Expand Down
7 changes: 6 additions & 1 deletion src/domain/service/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,8 @@ export default class NoteService {
}

/**
* Reutrn a tree structure of notes with childNotes for the given note id
* Reutrn a tree structure of notes with childNotes for the given note id.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Reutrn a tree structure of notes with childNotes for the given note id.
* Return a tree structure of notes with childNotes for the given note id.

* The sidebar position is returned on the root node only.
* @param noteId - id of the note to get structure
* @returns - Object of notes.
*/
Expand All @@ -534,6 +535,7 @@ export default class NoteService {
if (!notesRows || notesRows.length === 0) {
return null;
}

// Step 1: Parse and initialize all notes
notesRows.forEach((note) => {
notesMap.set(note.noteId, {
Expand All @@ -547,6 +549,9 @@ export default class NoteService {
notesRows.forEach((note) => {
if (note.parentId === null) {
root = notesMap.get(note.noteId) ?? null;
if (root !== null) {
root.sidebarPosition = note.sidebarPosition;
}
} else {
const parent = notesMap.get(note.parentId);

Expand Down
62 changes: 62 additions & 0 deletions src/presentation/http/router/note.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2298,6 +2298,7 @@ describe('Note API', () => {
await global.db.insertNoteSetting({
noteId: note.id,
isPublic: true,
sidebarPosition: 'edge',
});

return {
Expand All @@ -2309,6 +2310,7 @@ describe('Note API', () => {
expected: (note: Note, childNote: Note | null) => ({
noteId: note.publicId,
noteTitle: 'text',
sidebarPosition: 'edge',
childNotes: childNote,
}),
},
Expand All @@ -2329,10 +2331,12 @@ describe('Note API', () => {
await global.db.insertNoteSetting({
noteId: childNote.id,
isPublic: true,
sidebarPosition: 'edge',
});
await global.db.insertNoteSetting({
noteId: parentNote.id,
isPublic: true,
sidebarPosition: 'none',
});
await global.db.insertNoteRelation({
noteId: childNote.id,
Expand All @@ -2347,6 +2351,7 @@ describe('Note API', () => {
expected: (note: Note, childNote: Note | null) => ({
noteId: note.publicId,
noteTitle: 'text',
sidebarPosition: 'none',
childNotes: [
{
noteId: childNote?.publicId,
Expand Down Expand Up @@ -2374,5 +2379,62 @@ describe('Note API', () => {
expected(note, childNote)
);
});

test('Uses parent sidebar position while linked and own position after unlinking', async () => {
const rootNote = await global.db.insertNote({
creatorId: user.id,
content: DEFAULT_NOTE_CONTENT,
});
const childNote = await global.db.insertNote({
creatorId: user.id,
content: DEFAULT_NOTE_CONTENT,
});

await global.db.insertNoteSetting({
noteId: rootNote.id,
isPublic: true,
sidebarPosition: 'none',
});
await global.db.insertNoteSetting({
noteId: childNote.id,
isPublic: true,
sidebarPosition: 'edge',
});
await global.db.insertNoteRelation({
noteId: childNote.id,
parentId: rootNote.id,
});

let response = await global.api?.fakeRequest({
method: 'GET',
headers: {
authorization: `Bearer ${accessToken}`,
},
url: `/note/note-hierarchy/${childNote.publicId}`,
});

expect(response?.json().noteHierarchy.sidebarPosition).toBe('none');

const detachResponse = await global.api?.fakeRequest({
method: 'DELETE',
headers: {
authorization: `Bearer ${accessToken}`,
},
url: `/note/${childNote.publicId}/relation`,
});

expect(detachResponse?.statusCode).toBe(200);
expect(detachResponse?.json().isDeleted).toBe(true);

response = await global.api?.fakeRequest({
method: 'GET',
headers: {
authorization: `Bearer ${accessToken}`,
},
url: `/note/note-hierarchy/${childNote.publicId}`,
});

expect(response?.json().noteHierarchy.sidebarPosition).toBe('edge');
});
});
});
4 changes: 4 additions & 0 deletions src/presentation/http/router/noteSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ describe('NoteSettings API', () => {
{
isPublic: false,
invitationHash: noteSettings.invitationHash,
sidebarPosition: 'content',
team:
[
{
Expand Down Expand Up @@ -260,6 +261,7 @@ describe('NoteSettings API', () => {
{
isPublic: false,
invitationHash: noteSettings.invitationHash,
sidebarPosition: 'content',
team:
[
{
Expand Down Expand Up @@ -506,6 +508,7 @@ describe('NoteSettings API', () => {
body: {
isPublic: false,
cover: 'new-image.png',
sidebarPosition: 'edge',
},
url: `/note-settings/${note.publicId}`,
});
Expand All @@ -515,6 +518,7 @@ describe('NoteSettings API', () => {
if (expectedStatusCode === 200) {
expect(response?.json().isPublic).toBe(false);
expect(response?.json().cover).toBe('new-image.png');
expect(response?.json().sidebarPosition).toBe('edge');
}
});

Expand Down
8 changes: 6 additions & 2 deletions src/presentation/http/router/noteSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ const NoteSettingsRouter: FastifyPluginCallback<NoteSettingsRouterOptions> = (fa
* Patch noteSettings by note id
*/
fastify.patch<{
Body: Pick<NoteSettings, 'customHostname' | 'isPublic' | 'cover'>;
Body: Pick<NoteSettings, 'customHostname' | 'isPublic' | 'cover' | 'sidebarPosition'>;
Params: {
notePublicId: NotePublicId;
};
Expand Down Expand Up @@ -246,6 +246,9 @@ const NoteSettingsRouter: FastifyPluginCallback<NoteSettingsRouterOptions> = (fa
cover: {
type: 'string',
},
sidebarPosition: {
$ref: 'NoteSettingsSchema#/properties/sidebarPosition',
},
},
},
response: {
Expand All @@ -263,12 +266,13 @@ const NoteSettingsRouter: FastifyPluginCallback<NoteSettingsRouterOptions> = (fa
/**
* @todo validate data
*/
const { customHostname, isPublic, cover } = request.body;
const { customHostname, isPublic, cover, sidebarPosition } = request.body;

const updatedNoteSettings = await noteSettingsService.patchNoteSettingsByNoteId(noteId, {
customHostname,
isPublic,
cover,
sidebarPosition,
});

if (updatedNoteSettings === null) {
Expand Down
4 changes: 4 additions & 0 deletions src/presentation/http/schema/NoteHierarchy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export const NoteHierarchySchema = {
type: 'string',
maxLength: 50,
},
sidebarPosition: {
type: 'string',
enum: ['edge', 'content', 'none'],
},
childNotes: {
type: 'array',
items: { $ref: 'NoteHierarchySchema#' },
Expand Down
5 changes: 5 additions & 0 deletions src/presentation/http/schema/NoteSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export const NoteSettingsSchema = {
cover: {
type: 'string',
},
sidebarPosition: {
type: 'string',
enum: ['edge', 'content', 'none'],
default: 'content',
},
team: {
type: 'array',
items: {
Expand Down
8 changes: 6 additions & 2 deletions src/repository/storage/postgres/orm/sequelize/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,11 @@ export default class NoteSequelizeStorage {
n.id AS "noteId",
n.content,
n.public_id AS "publicId",
nr.parent_id AS "parentId"
nr.parent_id AS "parentId",
ns.sidebar_position AS "sidebarPosition"
FROM ${String(this.database.literal(this.tableName).val)} n
LEFT JOIN ${String(this.database.literal('note_relations').val)} nr ON n.id = nr.note_id
LEFT JOIN ${String(this.database.literal('note_settings').val)} ns ON n.id = ns.note_id
WHERE n.id = :startNoteId

UNION ALL
Expand All @@ -420,10 +422,12 @@ export default class NoteSequelizeStorage {
n.id AS "noteId",
n.content,
n.public_id AS "publicId",
nr.parent_id AS "parentId"
nr.parent_id AS "parentId",
ns.sidebar_position AS "sidebarPosition"
FROM ${String(this.database.literal(this.tableName).val)} n
INNER JOIN ${String(this.database.literal('note_relations').val)} nr ON n.id = nr.note_id
INNER JOIN note_tree nt ON nr.parent_id = nt."noteId"
LEFT JOIN ${String(this.database.literal('note_settings').val)} ns ON n.id = ns.note_id
)
SELECT * FROM note_tree;
`;
Expand Down
10 changes: 10 additions & 0 deletions src/repository/storage/postgres/orm/sequelize/noteSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export class NoteSettingsModel extends Model<InferAttributes<NoteSettingsModel>,
* Id of the cover file
*/
public declare cover: CreationOptional<NoteSettings['cover']>;

/**
* Position of the note in the sidebar
*/
public declare sidebarPosition: CreationOptional<NoteSettings['sidebarPosition']>;
}

/**
Expand Down Expand Up @@ -105,6 +110,11 @@ export default class NoteSettingsSequelizeStorage {
type: DataTypes.STRING,
allowNull: true,
},
sidebarPosition: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'content',
},
}, {
tableName: this.tableName,
sequelize: this.database,
Expand Down
4 changes: 3 additions & 1 deletion src/tests/utils/database-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type NoteSettingsMockCreationAttributes = {
isPublic: NoteSettings['isPublic'];
invitationHash?: NoteSettings['invitationHash'];
cover?: NoteSettings['cover'];
sidebarPosition?: NoteSettings['sidebarPosition'];
};

/**
Expand Down Expand Up @@ -232,10 +233,11 @@ export default class DatabaseHelpers {
const customHostname = noteSettings.customHostname ?? null;
const invitationHash = noteSettings.invitationHash ?? createInvitationHash();
const cover = noteSettings.cover ?? '';
const sidebarPosition = noteSettings.sidebarPosition ?? 'content';

noteSettings.invitationHash = invitationHash;

await this.orm.connection.query(`INSERT INTO public.note_settings ("note_id", "custom_hostname", "is_public", "invitation_hash", "cover") VALUES (${noteSettings.noteId}, '${customHostname}', ${noteSettings.isPublic}, '${invitationHash}', '${cover}')`);
await this.orm.connection.query(`INSERT INTO public.note_settings ("note_id", "custom_hostname", "is_public", "invitation_hash", "cover", "sidebar_position") VALUES (${noteSettings.noteId}, '${customHostname}', ${noteSettings.isPublic}, '${invitationHash}', '${cover}', '${sidebarPosition}')`);

return noteSettings;
}
Expand Down
Loading