feat(ui): add drag-to-reorder for pinned notes in sidebar (#25677)
This commit is contained in:
3 files changed
+102
-45
No files matched your search
@@ -78,6 +78,7 @@
|
|||||||
import FolderModal from './Sidebar/Folders/FolderModal.svelte';
|
import FolderModal from './Sidebar/Folders/FolderModal.svelte';
|
||||||
import Sidebar from '../icons/Sidebar.svelte';
|
import Sidebar from '../icons/Sidebar.svelte';
|
||||||
import PinnedModelList from './Sidebar/PinnedModelList.svelte';
|
import PinnedModelList from './Sidebar/PinnedModelList.svelte';
|
||||||
|
import PinnedNoteList from './Sidebar/PinnedNoteList.svelte';
|
||||||
import Note from '../icons/Note.svelte';
|
import Note from '../icons/Note.svelte';
|
||||||
import Code from '../icons/Code.svelte';
|
import Code from '../icons/Code.svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
@@ -1257,51 +1258,7 @@
|
|||||||
}}
|
}}
|
||||||
onAddLabel={$i18n.t('New Note')}
|
onAddLabel={$i18n.t('New Note')}
|
||||||
>
|
>
|
||||||
<div class="mt-0.5 pb-1.5">
|
<PinnedNoteList bind:selectedChatId />
|
||||||
{#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>
|
|
||||||
</Folder>
|
</Folder>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -234,6 +234,7 @@ type Settings = {
|
|||||||
renderMarkdownInAssistantMessages?: boolean;
|
renderMarkdownInAssistantMessages?: boolean;
|
||||||
recentEmojis?: string[];
|
recentEmojis?: string[];
|
||||||
pinnedMenuItems?: string[];
|
pinnedMenuItems?: string[];
|
||||||
|
pinnedNotesOrder?: string[];
|
||||||
|
|
||||||
system?: string;
|
system?: string;
|
||||||
seed?: number;
|
seed?: number;
|
||||||
|
|||||||
Reference in new issue
Block a user