Skip to content
Merged
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
4 changes: 0 additions & 4 deletions backend/boox_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,6 @@ func syncBooxWorkspace(app *pocketbase.PocketBase, workspaceId string, configRec
record.Set("childrenViewMode", "gallery")
record.Set("isDailyNote", false)
record.Set("isExpanded", false)
record.Set("isPinned", false)
record.Set("pinnedOrder", 0)
record.Set("showChildrenInSidebar", false)
record.Set("content", "")
record.Set("excerpt", "")
Expand Down Expand Up @@ -422,8 +420,6 @@ func ensureBooxRootPage(app *pocketbase.PocketBase, workspaceId string) (*core.R
record.Set("childrenViewMode", "gallery")
record.Set("isDailyNote", false)
record.Set("isExpanded", true)
record.Set("isPinned", false)
record.Set("pinnedOrder", 0)
record.Set("showChildrenInSidebar", true)
record.Set("isReadOnly", true)
record.Set("sourceOrigin", booxSourceOrigin)
Expand Down
2 changes: 2 additions & 0 deletions backend/migrations/001_initial_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func init() {
&core.TextField{Name: "excerpt", Max: 500},
&core.TextField{Name: "bodyText", Max: bodyTextFieldMax},
&core.NumberField{Name: "order"},
&core.BoolField{Name: "isTopLevel"},
&core.TextField{Name: "icon", Max: 50},
&core.TextField{Name: "color", Max: 20},
// Cover image support
Expand Down Expand Up @@ -239,6 +240,7 @@ func init() {
})
pages.AddIndex("idx_pages_workspace", false, "workspace", "")
pages.AddIndex("idx_pages_parentId", false, "parentId", "")
pages.AddIndex("idx_pages_workspace_parent_top", false, "workspace,parentId,isTopLevel", "")
pages.AddIndex("idx_pages_viewMode", false, "viewMode", "")
pages.AddIndex("idx_pages_workspace_viewMode", false, "workspace, viewMode", "")
pages.AddIndex("idx_pages_dailyNoteDate", false, "dailyNoteDate", "")
Expand Down
44 changes: 44 additions & 0 deletions backend/migrations/023_add_pages_is_top_level.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package migrations

import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)

// 023_add_pages_is_top_level distinguishes canonical top-level pages from
// unfiled parentless pages that belong in Inbox.
func init() {
m.Register(func(app core.App) error {
pages, err := app.FindCollectionByNameOrId("pages")
if err != nil {
return nil
}

needsField := pages.Fields.GetByName("isTopLevel") == nil
if needsField {
pages.Fields.Add(&core.BoolField{Name: "isTopLevel"})
pages.AddIndex("idx_pages_workspace_parent_top", false, "workspace,parentId,isTopLevel", "")
if err := app.Save(pages); err != nil {
return err
}
}

if !needsField {
return nil
}

records, err := app.FindAllRecords("pages")
if err != nil {
return nil
}

for _, record := range records {
record.Set("isTopLevel", record.GetString("parentId") == "")
if err := app.Save(record); err != nil {
return err
}
}

return nil
}, nil)
}
44 changes: 44 additions & 0 deletions backend/migrations/024_remove_pinned_fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package migrations

import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)

// 024_remove_pinned_fields drops the isPinned and pinnedOrder fields and their
// indexes from the pages collection. The "favorites" feature has been removed;
// top-level pages are now managed via the isTopLevel field instead.
func init() {
m.Register(func(app core.App) error {
pages, err := app.FindCollectionByNameOrId("pages")
if err != nil {
return nil
}

changed := false

// Drop isPinned field if it exists
if field := pages.Fields.GetByName("isPinned"); field != nil {
pages.Fields.RemoveById(field.GetId())
changed = true
}

// Drop pinnedOrder field if it exists
if field := pages.Fields.GetByName("pinnedOrder"); field != nil {
pages.Fields.RemoveById(field.GetId())
changed = true
}

// Remove index on isPinned
pages.RemoveIndex("idx_pages_isPinned")

// Remove index on pinnedOrder
pages.RemoveIndex("idx_pages_pinnedOrder")

if !changed {
return nil
}

return app.Save(pages)
}, nil)
}
2 changes: 2 additions & 0 deletions backend/migrations/migrations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func TestMigrationsExist(t *testing.T) {
"003_data_cleanup",
"022_remove_plan_limits",
"021_restore_post_v17_schema",
"023_add_pages_is_top_level",
}

for _, name := range expectedMigrations {
Expand Down Expand Up @@ -64,6 +65,7 @@ func TestPagesCollectionFields(t *testing.T) {
"bodyText",
"workspace",
"parentId",
"isTopLevel",
"order",
"icon",
"color",
Expand Down
2 changes: 1 addition & 1 deletion backend/templates/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func applyContent(app core.App, userId string, forceReset bool) error {
page.Set("childrenViewMode", pt.ChildrenViewMode)
page.Set("createdBy", userId)
page.Set("isExpanded", true)
page.Set("isPinned", pt.IsPinned)
page.Set("isTopLevel", pt.IsTopLevel)

// Set cover gradient if provided
if pt.CoverGradient != "" {
Expand Down
11 changes: 5 additions & 6 deletions backend/templates/new_user_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type PageTemplate struct {
TasksGroupBy string // 'section', 'dueDate', 'priority'
Tasks []TaskTemplate
Children []PageTemplate
IsPinned bool
IsTopLevel bool
}

// TaskTemplate defines the structure for creating a new task
Expand Down Expand Up @@ -81,7 +81,7 @@ func GetDefaultWorkspace() WorkspaceTemplate {
CoverAttribution: "Photo by John Doe on Unsplash",
Order: 0,
ChildrenViewMode: "gallery",
IsPinned: true,
IsTopLevel: true,
Children: []PageTemplate{
{
Title: "Product Launch Q1",
Expand Down Expand Up @@ -247,7 +247,7 @@ func GetDefaultWorkspace() WorkspaceTemplate {
CoverImage: "https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=1400&q=80",
Order: 1,
ChildrenViewMode: "list",
IsPinned: true,
IsTopLevel: true,
Children: []PageTemplate{
{
Title: "Daily Habits",
Expand Down Expand Up @@ -477,7 +477,7 @@ func GetDefaultWorkspace() WorkspaceTemplate {
Color: "#10b981",
CoverImage: "https://images.unsplash.com/photo-1557672172-298e090bd0f1?w=1400&q=80",
Order: 3,
IsPinned: true,
IsTopLevel: true,
ChildrenViewMode: "gallery",
Children: []PageTemplate{
{
Expand Down Expand Up @@ -515,7 +515,7 @@ func GetDefaultWorkspace() WorkspaceTemplate {
Order: 4,
Content: gettingStartedContent,
Excerpt: "Learn how to make the most of Planneer",
IsPinned: true,
IsTopLevel: true,
Children: []PageTemplate{
{
Title: "Keyboard Shortcuts",
Expand Down Expand Up @@ -1496,7 +1496,6 @@ var emptyNoteContent = mustMarshal(map[string]interface{}{
},
})


var keyboardShortcutsContent = mustMarshal(map[string]interface{}{
"heading": map[string]interface{}{
"id": "heading",
Expand Down
46 changes: 13 additions & 33 deletions frontend/src/api/pagesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ import { devLog, devWarn } from '@/lib/config';
export interface FetchPagesOptions {
/** Only fetch root-level pages (parentId is null/empty). Mutually exclusive with parentId. */
rootOnly?: boolean;
/** Only fetch parentless Inbox pages. Mutually exclusive with rootOnly and parentId. */
inboxOnly?: boolean;
/** Fetch children of a specific parent ID. Mutually exclusive with rootOnly. */
parentId?: string;
/** Filter by view mode (e.g., 'tasks' to get only task collections) */
Expand Down Expand Up @@ -135,8 +137,14 @@ export async function fetchPages(options?: FetchPagesOptions): Promise<FetchPage
const filters: string[] = [];

if (options?.rootOnly) {
// Root-only query for sidebar tree - exclude daily notes since they don't appear in the tree
// Root-only query for sidebar tree - exclude daily notes and Inbox pages.
filters.push(`(parentId = '' || parentId = null)`);
filters.push(`isTopLevel = true`);
filters.push(`isDailyNote = false`);
}
if (options?.inboxOnly) {
filters.push(`(parentId = '' || parentId = null)`);
filters.push(`isTopLevel = false`);
filters.push(`isDailyNote = false`);
}
if (options?.parentId) {
Expand Down Expand Up @@ -420,34 +428,6 @@ export async function fetchAllPagesMetadata(): Promise<Page[]> {
}
}

/**
* Fetch pinned pages specifically - ensures pinned pages at any level are loaded.
* Used as a supplementary fetch during initial load.
*/
export async function fetchPinnedPages(): Promise<Page[]> {
const workspaceId = getCurrentWorkspaceIdOrNull();
if (!workspaceId) return [];

try {
const result = await pb.collection('pages').getFullList({
filter: buildWorkspaceFilter(workspaceId, 'isPinned = true'),
sort: 'pinnedOrder',
fields: METADATA_FIELDS,
});

const pages = result.map(record => ({
...pbToFrontend<Page>(record as Record<string, unknown>),
content: null,
}));

devLog(`[pagesApi] Fetched ${pages.length} pinned pages`);
return pages;
} catch (e) {
console.error('[pagesApi] fetchPinnedPages error:', e);
return [];
}
}

// ============================================================================
// CRUD OPERATIONS
// ============================================================================
Expand Down Expand Up @@ -490,7 +470,7 @@ export interface PagePatchRequest {
/** Block IDs with only order changes - maps to new order value (bandwidth optimization) */
blockOrders?: Record<string, number>;
/** Metadata fields to update (title, parentId, etc.) */
metadata?: Partial<Pick<Page, 'title' | 'parentId' | 'icon' | 'color' | 'order' | 'viewMode' | 'childrenViewMode' | 'isExpanded' | 'isPinned' | 'isDailyNote' | 'dailyNoteDate' | 'excerpt' | 'collectionSortBy' | 'collectionSortDirection' | 'collectionGroupBy' | 'sections' | 'tasksViewMode' | 'tasksGroupBy' | 'showCompletedTasks' | 'showExcerpts' | 'savedViews' | 'activeSavedViewId'>>;
metadata?: Partial<Pick<Page, 'title' | 'parentId' | 'isTopLevel' | 'icon' | 'color' | 'order' | 'viewMode' | 'childrenViewMode' | 'isExpanded' | 'isDailyNote' | 'dailyNoteDate' | 'excerpt' | 'collectionSortBy' | 'collectionSortDirection' | 'collectionGroupBy' | 'sections' | 'tasksViewMode' | 'tasksGroupBy' | 'showCompletedTasks' | 'showExcerpts' | 'savedViews' | 'activeSavedViewId'>>;
}

/**
Expand Down Expand Up @@ -524,8 +504,8 @@ export async function deletePage(id: string): Promise<void> {
// ============================================================================

/**
* Move orphaned child pages to root level when their parent is deleted.
* Returns the IDs of pages that were moved to root level.
* Move orphaned child pages to Inbox when their parent is deleted.
* Returns the IDs of pages that were moved to Inbox.
*/
export async function moveOrphanedPagesToRoot(
parentId: string
Expand All @@ -540,7 +520,7 @@ export async function moveOrphanedPagesToRoot(
// Update each to remove parent
await Promise.all(
children.map((p) =>
updatePage(p.id, { parentId: null })
updatePage(p.id, { parentId: null, isTopLevel: false })
)
);

Expand Down
13 changes: 13 additions & 0 deletions frontend/src/components/common/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
PlusIcon,
CheckIcon,
PagesIcon,
InboxIcon,
HomeIcon,
CalendarIcon,
ClockIcon,
Expand Down Expand Up @@ -321,6 +322,18 @@ const CommandPalette: React.FC<CommandPaletteProps> = ({
onClose();
},
},
{
id: 'nav-inbox',
category: 'navigation',
icon: <InboxIcon className="w-5 h-5" />,
title: 'Go to Inbox',
subtitle: 'Review unfiled pages',
keywords: ['inbox', 'unfiled', 'pages'],
onSelect: () => {
navigate({ to: '/inbox' });
onClose();
},
},
{
id: 'nav-tasks',
category: 'navigation',
Expand Down
24 changes: 23 additions & 1 deletion frontend/src/components/common/MoveToParentPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useIsMobile } from '@frameer/hooks/useMobileDetection';
import { StylizedCollectionIcon } from '@/components/common/StylizedIcons';
import { fetchPages } from '@/api/pagesApi';
import type { Page } from '@/types/page';
import { Folder, Loader2, ChevronRight } from 'lucide-react';
import { Folder, Inbox, Loader2, ChevronRight } from 'lucide-react';

interface MoveToParentPickerProps {
isOpen: boolean;
Expand Down Expand Up @@ -72,6 +72,11 @@ export const MoveToParentPicker: React.FC<MoveToParentPickerProps> = ({
movePage(pageId, null);
onClose();
};

const handleMoveToInbox = () => {
movePage(pageId, null, { isTopLevel: false });
onClose();
};

// Get parent title for breadcrumb display (one level only)
const getParentBreadcrumb = (page: Page): string | null => {
Expand Down Expand Up @@ -112,6 +117,23 @@ export const MoveToParentPicker: React.FC<MoveToParentPickerProps> = ({
</div>
</div>
</button>

<button
onClick={handleMoveToInbox}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left hover:bg-[var(--color-surface-secondary)] transition-colors"
>
<div className="w-8 h-8 flex items-center justify-center rounded-lg bg-[var(--color-surface-secondary)]">
<Inbox className="w-5 h-5 text-[var(--color-text-secondary)]" />
</div>
<div>
<div className="text-sm font-medium text-[var(--color-text-primary)]">
Inbox
</div>
<div className="text-xs text-[var(--color-text-secondary)]">
Keep it unfiled until you organize it
</div>
</div>
</button>

{loading ? (
<div className="flex items-center justify-center py-8">
Expand Down
Loading
Loading