mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Add database migration and model changes for Star folders
- Add parentId and isFolder columns to stars table - Add self-referential relationship for hierarchical structure - Add database constraints to ensure folders don't have content - Add indexes for efficient folder queries - Update server and client Star models with new fields - Add helper methods for folder operations - Update Star presenter to include new fields This enables users to organize their starred items into folders while maintaining backward compatibility with existing stars.
This commit is contained in:
+35
-1
@@ -14,6 +14,16 @@ class Star extends Model {
|
||||
@observable
|
||||
index: string;
|
||||
|
||||
/** Whether this star represents a folder */
|
||||
@Field
|
||||
@observable
|
||||
isFolder: boolean;
|
||||
|
||||
/** The parent folder ID */
|
||||
@Field
|
||||
@observable
|
||||
parentId?: string;
|
||||
|
||||
/** The document ID that is starred. */
|
||||
documentId?: string;
|
||||
|
||||
@@ -43,7 +53,31 @@ class Star extends Model {
|
||||
*/
|
||||
previous(): Star | undefined {
|
||||
const index = this.store.orderedData.indexOf(this);
|
||||
return this.store.orderedData[index + 1];
|
||||
return this.store.orderedData[index - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this star is a folder.
|
||||
*/
|
||||
get isStarFolder(): boolean {
|
||||
return this.isFolder === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the children of this folder, or empty array if not a folder.
|
||||
*/
|
||||
get folderChildren(): Star[] {
|
||||
if (!this.isStarFolder) {
|
||||
return [];
|
||||
}
|
||||
return this.store.orderedData.filter(star => star.parentId === this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this star has content (document or collection).
|
||||
*/
|
||||
get hasContent(): boolean {
|
||||
return !!(this.documentId || this.collectionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// Add parentId column for hierarchical structure
|
||||
await queryInterface.addColumn("stars", "parentId", {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: "stars",
|
||||
key: "id",
|
||||
},
|
||||
onDelete: "CASCADE",
|
||||
});
|
||||
|
||||
// Add isFolder column to distinguish folders from regular stars
|
||||
await queryInterface.addColumn("stars", "isFolder", {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
// Add index for efficient parent-child queries
|
||||
await queryInterface.addIndex("stars", ["parentId"], {
|
||||
name: "stars_parent_id",
|
||||
});
|
||||
|
||||
// Add composite index for user-specific folder queries
|
||||
await queryInterface.addIndex("stars", ["userId", "parentId"], {
|
||||
name: "stars_user_id_parent_id",
|
||||
});
|
||||
|
||||
// Add composite index for filtering folders vs regular stars
|
||||
await queryInterface.addIndex("stars", ["userId", "isFolder"], {
|
||||
name: "stars_user_id_is_folder",
|
||||
});
|
||||
|
||||
// Add check constraint to ensure folders don't have documentId or collectionId
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE stars ADD CONSTRAINT stars_folder_content_check
|
||||
CHECK (
|
||||
(isFolder = true AND documentId IS NULL AND collectionId IS NULL) OR
|
||||
(isFolder = false)
|
||||
)
|
||||
`);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// Remove check constraint
|
||||
await queryInterface.sequelize.query(`
|
||||
ALTER TABLE stars DROP CONSTRAINT IF EXISTS stars_folder_content_check
|
||||
`);
|
||||
|
||||
// Remove indexes
|
||||
await queryInterface.removeIndex("stars", "stars_user_id_is_folder");
|
||||
await queryInterface.removeIndex("stars", "stars_user_id_parent_id");
|
||||
await queryInterface.removeIndex("stars", "stars_parent_id");
|
||||
|
||||
// Remove columns
|
||||
await queryInterface.removeColumn("stars", "isFolder");
|
||||
await queryInterface.removeColumn("stars", "parentId");
|
||||
},
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ForeignKey,
|
||||
Table,
|
||||
Length,
|
||||
HasMany,
|
||||
} from "sequelize-typescript";
|
||||
import Collection from "./Collection";
|
||||
import Document from "./Document";
|
||||
@@ -26,6 +27,9 @@ class Star extends IdModel<
|
||||
@Column
|
||||
index: string | null;
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
isFolder: boolean;
|
||||
|
||||
// associations
|
||||
|
||||
@BelongsTo(() => User, "userId")
|
||||
@@ -48,6 +52,16 @@ class Star extends IdModel<
|
||||
@ForeignKey(() => Collection)
|
||||
@Column(DataType.UUID)
|
||||
collectionId: string | null;
|
||||
|
||||
@BelongsTo(() => Star, "parentId")
|
||||
parent: Star | null;
|
||||
|
||||
@ForeignKey(() => Star)
|
||||
@Column(DataType.UUID)
|
||||
parentId: string | null;
|
||||
|
||||
@HasMany(() => Star, "parentId")
|
||||
children: Star[];
|
||||
}
|
||||
|
||||
export default Star;
|
||||
|
||||
@@ -5,6 +5,8 @@ export default function presentStar(star: Star) {
|
||||
id: star.id,
|
||||
documentId: star.documentId,
|
||||
collectionId: star.collectionId,
|
||||
parentId: star.parentId,
|
||||
isFolder: star.isFolder,
|
||||
index: star.index,
|
||||
createdAt: star.createdAt,
|
||||
updatedAt: star.updatedAt,
|
||||
|
||||
Reference in New Issue
Block a user