feat(ui): add drag-to-reorder for pinned notes in sidebar (#25677)

This commit is contained in:
G30 2026-06-29 04:22:33 -04:00 committed by GitHub
parent 3fd0384ffc
commit 70b89d01c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 102 additions and 45 deletions

View file

@ -78,6 +78,7 @@
import FolderModal from './Sidebar/Folders/FolderModal.svelte';
import Sidebar from '../icons/Sidebar.svelte';
import PinnedModelList from './Sidebar/PinnedModelList.svelte';
import PinnedNoteList from './Sidebar/PinnedNoteList.svelte';
import Note from '../icons/Note.svelte';
import Code from '../icons/Code.svelte';
import { slide } from 'svelte/transition';
@ -1257,51 +1258,7 @@
}}
onAddLabel={$i18n.t('New Note')}
>
<div class="mt-0.5 pb-1.5">
{#each $pinnedNotes as note (note.id)}
<a
class="w-full flex items-center gap-2.5 rounded-xl px-2.5 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition group text-sm"
href={`/notes/${note.id}`}
on:click={() => {
itemClickHandler();
}}
draggable="false"
>
<div class="self-center">
<Note className="size-4" strokeWidth="2" />
</div>
<div class="flex-1 text-ellipsis line-clamp-1">
{note.title}
</div>
<button
class="invisible group-hover:visible self-center p-0.5 hover:bg-gray-200 dark:hover:bg-gray-800 rounded-lg transition"
on:click|preventDefault|stopPropagation={async () => {
await toggleNotePinnedStatusById(localStorage.token, note.id);
const _pinnedNotes = await getPinnedNoteList(localStorage.token).catch(
() => []
);
pinnedNotes.set(_pinnedNotes);
}}
aria-label={$i18n.t('Unpin')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-3.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
</a>
{/each}
</div>
<PinnedNoteList bind:selectedChatId />
</Folder>
{/if}

View file

@ -0,0 +1,99 @@
<script>
import Sortable from 'sortablejs';
import { onMount, getContext, tick } from 'svelte';
import { chatId, mobile, pinnedNotes, settings, showSidebar } from '$lib/stores';
import { updateUserSettings } from '$lib/apis/users';
import { getPinnedNoteList, toggleNotePinnedStatusById } from '$lib/apis/notes';
import Note from '$lib/components/icons/Note.svelte';
const i18n = getContext('i18n');
export let selectedChatId = null;
$: sortedPinnedNotes = (() => {
const order = $settings?.pinnedNotesOrder;
if (!order || order.length === 0) return $pinnedNotes;
const orderMap = new Map(order.map((id, idx) => [id, idx]));
return [...$pinnedNotes].sort((a, b) => {
const aIdx = orderMap.has(a.id) ? orderMap.get(a.id) : Infinity;
const bIdx = orderMap.has(b.id) ? orderMap.get(b.id) : Infinity;
return aIdx - bIdx;
});
})();
const initPinnedNotesSortable = () => {
const pinnedNotesList = document.getElementById('pinned-notes-list');
if (pinnedNotesList && !$mobile) {
new Sortable(pinnedNotesList, {
animation: 150,
onUpdate: async (event) => {
const noteId = event.item.dataset.id;
const newIndex = event.newIndex;
const current = sortedPinnedNotes.map((n) => n.id);
const oldIndex = current.indexOf(noteId);
current.splice(oldIndex, 1);
current.splice(newIndex, 0, noteId);
settings.set({ ...$settings, pinnedNotesOrder: current });
await updateUserSettings(localStorage.token, { ui: $settings });
}
});
}
};
onMount(async () => {
await tick();
initPinnedNotesSortable();
});
</script>
<div class="mt-0.5 pb-1.5" id="pinned-notes-list">
{#each sortedPinnedNotes as note (note.id)}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex items-center text-gray-800 dark:text-gray-200 cursor-grab relative group rounded-xl px-2.5 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
data-id={note.id}
>
<a
class="grow flex items-center gap-2.5 text-sm"
href={`/notes/${note.id}`}
on:click={() => {
selectedChatId = null;
chatId.set('');
if ($mobile) {
showSidebar.set(false);
}
}}
draggable="false"
>
<div class="self-center">
<Note className="size-4" strokeWidth="2" />
</div>
<div class="flex-1 text-ellipsis line-clamp-1">
{note.title}
</div>
</a>
<button
class="invisible group-hover:visible self-center p-0.5 hover:bg-gray-200 dark:hover:bg-gray-800 rounded-lg transition"
on:click|preventDefault|stopPropagation={async () => {
await toggleNotePinnedStatusById(localStorage.token, note.id);
const _pinnedNotes = await getPinnedNoteList(localStorage.token).catch(() => []);
pinnedNotes.set(_pinnedNotes);
}}
aria-label={$i18n.t('Unpin')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-3.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
{/each}
</div>

View file

@ -234,6 +234,7 @@ type Settings = {
renderMarkdownInAssistantMessages?: boolean;
recentEmojis?: string[];
pinnedMenuItems?: string[];
pinnedNotesOrder?: string[];
system?: string;
seed?: number;