Added Note Sharing Links
This commit is contained in:
+56
-46
@@ -92,53 +92,63 @@ const App: Component = () => {
|
|||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={isEmbed()}
|
when={isEmbed()}
|
||||||
fallback={
|
fallback={
|
||||||
<Show when={isAuthenticated()} fallback={<AuthCallback onSuccess={() => { }} />}>
|
<Show when={isAuthenticated()} fallback={<AuthCallback onSuccess={() => { }} />}>
|
||||||
<Layout currentView={currentView()} setView={setCurrentView}>
|
<Layout currentView={currentView()} setView={setCurrentView}>
|
||||||
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
||||||
<Show when={currentView() === "critical"}><CriticalView /></Show>
|
<Show when={currentView() === "critical"}><CriticalView /></Show>
|
||||||
<Show when={currentView() === "urgency"}><UrgencyView /></Show>
|
<Show when={currentView() === "urgency"}><UrgencyView /></Show>
|
||||||
<Show when={currentView() === "priority"}><PriorityView /></Show>
|
<Show when={currentView() === "priority"}><PriorityView /></Show>
|
||||||
<Show when={currentView() === "matrix"}><MatrixView /></Show>
|
<Show when={currentView() === "matrix"}><MatrixView /></Show>
|
||||||
<Show when={currentView() === "snowball"}><SnowballView /></Show>
|
<Show when={currentView() === "snowball"}><SnowballView /></Show>
|
||||||
<Show when={currentView() === "dig_in"}><DigInView /></Show>
|
<Show when={currentView() === "dig_in"}><DigInView /></Show>
|
||||||
<Show when={currentView() === "progress"}><ProgressView /></Show>
|
<Show when={currentView() === "progress"}><ProgressView /></Show>
|
||||||
<Show when={currentView() === "settings"}><SettingsView setView={setCurrentView} /></Show>
|
<Show when={currentView() === "settings"}><SettingsView setView={setCurrentView} /></Show>
|
||||||
<Show when={currentView() === "help"}><HelpView onBack={() => setCurrentView("settings")} /></Show>
|
<Show when={currentView() === "help"}><HelpView onBack={() => setCurrentView("settings")} /></Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(() => {
|
||||||
|
const isPublicNote = () => {
|
||||||
|
const view = getEmbedView();
|
||||||
|
const urlObj = new URL(location());
|
||||||
|
const noteId = new URLSearchParams(urlObj.search).get('noteId') ||
|
||||||
|
(urlObj.hash.includes('?') ? new URLSearchParams(urlObj.hash.substring(urlObj.hash.indexOf('?'))).get('noteId') : null);
|
||||||
|
return view === 'embed_notes' && !!noteId;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmbedAuthWrapper skipAuth={isPublicNote()}>
|
||||||
|
<EmbedLayout>
|
||||||
|
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
||||||
|
<Show when={getEmbedView() === 'embed_notes'}>
|
||||||
|
{(() => {
|
||||||
|
const noteIdFromUrl = createMemo(() => {
|
||||||
|
const urlObj = new URL(location());
|
||||||
|
let noteId = new URLSearchParams(urlObj.search).get('noteId');
|
||||||
|
if (!noteId && urlObj.hash.includes('?')) {
|
||||||
|
const hashQueryString = urlObj.hash.substring(urlObj.hash.indexOf('?'));
|
||||||
|
noteId = new URLSearchParams(hashQueryString).get('noteId');
|
||||||
|
}
|
||||||
|
return noteId;
|
||||||
|
});
|
||||||
|
return <NotepadView selectedNoteId={noteIdFromUrl()} setSelectedNoteId={() => { }} hideNavigation={!!noteIdFromUrl()} />;
|
||||||
|
})()}
|
||||||
|
</Show>
|
||||||
|
<Show when={getEmbedView() === 'embed_quick_add'}>
|
||||||
|
<QuickEntryForm autoFocus={true} />
|
||||||
|
</Show>
|
||||||
|
</Suspense>
|
||||||
|
</EmbedLayout>
|
||||||
|
</EmbedAuthWrapper>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</Show>
|
</Show>
|
||||||
}
|
|
||||||
>
|
|
||||||
<EmbedAuthWrapper>
|
|
||||||
<EmbedLayout>
|
|
||||||
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
|
||||||
<Show when={getEmbedView() === 'embed_notes'}>
|
|
||||||
{(() => {
|
|
||||||
const noteIdFromUrl = createMemo(() => {
|
|
||||||
const urlObj = new URL(location());
|
|
||||||
let noteId = new URLSearchParams(urlObj.search).get('noteId');
|
|
||||||
// Production likely uses hash routing, so check hash for query params too
|
|
||||||
if (!noteId && urlObj.hash.includes('?')) {
|
|
||||||
const hashQueryString = urlObj.hash.substring(urlObj.hash.indexOf('?'));
|
|
||||||
noteId = new URLSearchParams(hashQueryString).get('noteId');
|
|
||||||
}
|
|
||||||
return noteId;
|
|
||||||
});
|
|
||||||
console.log('[DEBUG App] Rendering embed/notes context. noteId trace:', noteIdFromUrl());
|
|
||||||
return <NotepadView selectedNoteId={noteIdFromUrl()} setSelectedNoteId={() => { }} hideNavigation={!!noteIdFromUrl()} />;
|
|
||||||
})()}
|
|
||||||
</Show>
|
|
||||||
<Show when={getEmbedView() === 'embed_quick_add'}>
|
|
||||||
<QuickEntryForm autoFocus={true} />
|
|
||||||
</Show>
|
|
||||||
</Suspense>
|
|
||||||
</EmbedLayout>
|
|
||||||
</EmbedAuthWrapper>
|
|
||||||
</Show>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { pb } from "@/lib/pocketbase";
|
|||||||
|
|
||||||
interface EmbedAuthWrapperProps {
|
interface EmbedAuthWrapperProps {
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
|
skipAuth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmbedAuthWrapper: Component<EmbedAuthWrapperProps> = (props) => {
|
export const EmbedAuthWrapper: Component<EmbedAuthWrapperProps> = (props) => {
|
||||||
const [isHydrated, setIsHydrated] = createSignal(pb.authStore.isValid);
|
const [isHydrated, setIsHydrated] = createSignal(pb.authStore.isValid || !!props.skipAuth);
|
||||||
|
|
||||||
const handleMessage = (event: MessageEvent) => {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
if (props.skipAuth) return; // Don't process auth messages in skip mode
|
||||||
const { data } = event;
|
const { data } = event;
|
||||||
console.log('[DEBUG] EmbedAuthWrapper received message:', data?.type);
|
console.log('[DEBUG] EmbedAuthWrapper received message:', data?.type);
|
||||||
if (data?.type === "TASGRID_AUTH" && data.token) {
|
if (data?.type === "TASGRID_AUTH" && data.token) {
|
||||||
@@ -20,7 +22,7 @@ export const EmbedAuthWrapper: Component<EmbedAuthWrapperProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
console.log('[DEBUG] EmbedAuthWrapper mounted. Initial isHydrated:', isHydrated());
|
console.log('[DEBUG] EmbedAuthWrapper mounted. Initial isHydrated:', isHydrated(), 'skipAuth:', props.skipAuth);
|
||||||
window.addEventListener("message", handleMessage);
|
window.addEventListener("message", handleMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ export const EmbedAuthWrapper: Component<EmbedAuthWrapperProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={isHydrated()}
|
when={isHydrated() || props.skipAuth}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="flex flex-col items-center justify-center h-full space-y-4">
|
<div class="flex flex-col items-center justify-center h-full space-y-4">
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
|||||||
@@ -714,6 +714,30 @@ const mapRecordToNote = (r: any): Note => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchNoteById = async (id: string): Promise<Note | null> => {
|
||||||
|
try {
|
||||||
|
const record = await pb.collection(NOTES_COLLECTION).getOne(id);
|
||||||
|
if (!record) return null;
|
||||||
|
return mapRecordToNote(record);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch note by ID:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateStoreWithNote = (note: Note) => {
|
||||||
|
setStore("notes", (currentNotes) => {
|
||||||
|
const index = currentNotes.findIndex(n => n.id === note.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
const newNotes = [...currentNotes];
|
||||||
|
newNotes[index] = note;
|
||||||
|
return newNotes;
|
||||||
|
}
|
||||||
|
return [...currentNotes, note];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Helper to check if a task should be visible based on ownership, shares, or rules
|
// Helper to check if a task should be visible based on ownership, shares, or rules
|
||||||
const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
|
||||||
// Own task
|
// Own task
|
||||||
|
|||||||
+77
-11
@@ -1,5 +1,5 @@
|
|||||||
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
|
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
|
||||||
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag } from "@/store";
|
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments, getFavoriteTag, fetchNoteById, updateStoreWithNote } from "@/store";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { type Note } from "@/store";
|
import { type Note } from "@/store";
|
||||||
import { Plus, X, Lock, Unlock, Trash2, ChevronDown, Link as LinkIcon, MoreHorizontal, Type, FileText, Search, Star, ChevronRight } from "lucide-solid";
|
import { Plus, X, Lock, Unlock, Trash2, ChevronDown, Link as LinkIcon, MoreHorizontal, Type, FileText, Search, Star, ChevronRight } from "lucide-solid";
|
||||||
@@ -28,7 +28,7 @@ export const NotepadView: Component<{
|
|||||||
const [noteActionsOpen, setNoteActionsOpen] = createSignal(false);
|
const [noteActionsOpen, setNoteActionsOpen] = createSignal(false);
|
||||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = createSignal(false);
|
const [confirmDeleteOpen, setConfirmDeleteOpen] = createSignal(false);
|
||||||
|
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = createMemo(() => pb.authStore.model?.id);
|
||||||
|
|
||||||
// Sync activeNoteId for slash commands and run cleanup on unmount/change
|
// Sync activeNoteId for slash commands and run cleanup on unmount/change
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -62,6 +62,25 @@ export const NotepadView: Component<{
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle fetching note for unauthenticated users (public shares)
|
||||||
|
createEffect(async () => {
|
||||||
|
const id = props.selectedNoteId;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
// If note is already in store, we are good
|
||||||
|
const exists = store.notes.find(n => n.id === id);
|
||||||
|
if (exists) return;
|
||||||
|
|
||||||
|
// If not authenticated or note just missing (e.g. direct link), try to fetch it
|
||||||
|
console.log("[NotepadView] Note not found in store, attempting public fetch for:", id);
|
||||||
|
const note = await fetchNoteById(id);
|
||||||
|
if (note) {
|
||||||
|
updateStoreWithNote(note);
|
||||||
|
} else if (!pb.authStore.isValid) {
|
||||||
|
toast.error("Note not found or you don't have permission to view it.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Derived states
|
// Derived states
|
||||||
const activeNote = createMemo(() => {
|
const activeNote = createMemo(() => {
|
||||||
const id = props.selectedNoteId?.trim();
|
const id = props.selectedNoteId?.trim();
|
||||||
@@ -206,7 +225,8 @@ export const NotepadView: Component<{
|
|||||||
|
|
||||||
const handleCreateTab = async () => {
|
const handleCreateTab = async () => {
|
||||||
const root = rootParentNote();
|
const root = rootParentNote();
|
||||||
if (!root || !currentUserId) return;
|
const userId = currentUserId();
|
||||||
|
if (!root || !userId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await pb.collection(NOTES_COLLECTION).create({
|
const result = await pb.collection(NOTES_COLLECTION).create({
|
||||||
@@ -214,7 +234,7 @@ export const NotepadView: Component<{
|
|||||||
content: "",
|
content: "",
|
||||||
tags: [`child-of-${root.id}`],
|
tags: [`child-of-${root.id}`],
|
||||||
isPrivate: root.isPrivate, // inherit privacy
|
isPrivate: root.isPrivate, // inherit privacy
|
||||||
user: currentUserId,
|
user: userId,
|
||||||
});
|
});
|
||||||
props.setSelectedNoteId(result.id);
|
props.setSelectedNoteId(result.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -333,8 +353,8 @@ export const NotepadView: Component<{
|
|||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
{(note) => {
|
{(note) => {
|
||||||
const isOwner = note().user === currentUserId;
|
const isOwner = createMemo(() => note().user === currentUserId());
|
||||||
const canEdit = isOwner || !note().isPrivate;
|
const canEdit = createMemo(() => isOwner() || !note().isPrivate);
|
||||||
|
|
||||||
const hasTopViewer = createMemo(() => {
|
const hasTopViewer = createMemo(() => {
|
||||||
const c = note()?.content;
|
const c = note()?.content;
|
||||||
@@ -383,7 +403,7 @@ export const NotepadView: Component<{
|
|||||||
<SortableDesktopTab
|
<SortableDesktopTab
|
||||||
tab={tab}
|
tab={tab}
|
||||||
isSelected={props.selectedNoteId === tab.id}
|
isSelected={props.selectedNoteId === tab.id}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -421,7 +441,7 @@ export const NotepadView: Component<{
|
|||||||
)}
|
)}
|
||||||
value={root().title}
|
value={root().title}
|
||||||
placeholder="Note Title"
|
placeholder="Note Title"
|
||||||
readOnly={!canEdit}
|
readOnly={!canEdit()}
|
||||||
rows={1}
|
rows={1}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
const val = e.currentTarget.value.trim() || 'Untitled';
|
const val = e.currentTarget.value.trim() || 'Untitled';
|
||||||
@@ -493,7 +513,7 @@ export const NotepadView: Component<{
|
|||||||
>
|
>
|
||||||
<Star size={14} class={cn(note().tags?.includes(getFavoriteTag()) && "fill-amber-500")} />
|
<Star size={14} class={cn(note().tags?.includes(getFavoriteTag()) && "fill-amber-500")} />
|
||||||
</Button>
|
</Button>
|
||||||
<Show when={isOwner}>
|
<Show when={isOwner()}>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
as={Button}
|
as={Button}
|
||||||
@@ -518,6 +538,29 @@ export const NotepadView: Component<{
|
|||||||
<><Lock size={14} /> Private</>
|
<><Lock size={14} /> Private</>
|
||||||
</Show>
|
</Show>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="w-full justify-start text-[0.625rem] font-bold uppercase tracking-widest gap-2 hover:bg-muted text-muted-foreground transition-all"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const url = `${window.location.origin}${window.location.pathname}#/embed/notes?noteId=${note().id}`;
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
toast.success("Share link copied to clipboard");
|
||||||
|
|
||||||
|
if (note().isPrivate) {
|
||||||
|
toast.info("Note is currently private. Make it public to allow others to see it?", {
|
||||||
|
action: {
|
||||||
|
label: "Make Public",
|
||||||
|
onClick: () => handleUpdateNote(note().id, { isPrivate: false })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinkIcon size={14} />
|
||||||
|
Share Link
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -557,7 +600,7 @@ export const NotepadView: Component<{
|
|||||||
content={note().content}
|
content={note().content}
|
||||||
onUpdate={(html) => handleUpdateContent(note().id, html)}
|
onUpdate={(html) => handleUpdateContent(note().id, html)}
|
||||||
onEditorReady={setEditorInstance}
|
onEditorReady={setEditorInstance}
|
||||||
editable={canEdit}
|
editable={canEdit()}
|
||||||
class={hasTopViewer() ? "[&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:px-4 md:[&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:px-6 [&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:max-w-3xl [&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:mx-auto [&>.file-viewer-wrapper:first-child]:h-[80vh] [&>.file-viewer-wrapper:first-child]:min-h-0 [&>.file-attachment-wrapper:first-child]:h-[80vh] [&>.file-attachment-wrapper:first-child]:min-h-0 pb-20" : ""}
|
class={hasTopViewer() ? "[&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:px-4 md:[&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:px-6 [&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:max-w-3xl [&>*:not(.file-viewer-wrapper:first-child,.file-attachment-wrapper:first-child)]:mx-auto [&>.file-viewer-wrapper:first-child]:h-[80vh] [&>.file-viewer-wrapper:first-child]:min-h-0 [&>.file-attachment-wrapper:first-child]:h-[80vh] [&>.file-attachment-wrapper:first-child]:min-h-0 pb-20" : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -699,7 +742,7 @@ export const NotepadView: Component<{
|
|||||||
<div class="px-4 py-4 border-t border-border/50 md:hidden flex items-center justify-between shrink-0 bg-background/95 backdrop-blur-sm z-40 w-full sticky bottom-0">
|
<div class="px-4 py-4 border-t border-border/50 md:hidden flex items-center justify-between shrink-0 bg-background/95 backdrop-blur-sm z-40 w-full sticky bottom-0">
|
||||||
<div class="flex items-center gap-2 w-16 shrink-0">
|
<div class="flex items-center gap-2 w-16 shrink-0">
|
||||||
{/* "..." popover menu containing Public/Private toggle and Delete */}
|
{/* "..." popover menu containing Public/Private toggle and Delete */}
|
||||||
<Show when={isOwner}>
|
<Show when={isOwner()}>
|
||||||
<Popover
|
<Popover
|
||||||
open={noteActionsOpen()}
|
open={noteActionsOpen()}
|
||||||
onOpenChange={(nextOpen) => {
|
onOpenChange={(nextOpen) => {
|
||||||
@@ -730,6 +773,29 @@ export const NotepadView: Component<{
|
|||||||
<><Lock size={14} /> Private</>
|
<><Lock size={14} /> Private</>
|
||||||
</Show>
|
</Show>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="w-full justify-start text-[0.625rem] font-bold uppercase tracking-widest gap-2 hover:bg-muted text-muted-foreground transition-all"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const url = `${window.location.origin}${window.location.pathname}#/embed/notes?noteId=${note().id}`;
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
toast.success("Share link copied to clipboard");
|
||||||
|
|
||||||
|
if (note().isPrivate) {
|
||||||
|
toast.info("Note is currently private. Make it public?", {
|
||||||
|
action: {
|
||||||
|
label: "Make Public",
|
||||||
|
onClick: () => handleUpdateNote(note().id, { isPrivate: false })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinkIcon size={14} />
|
||||||
|
Share Link
|
||||||
|
</Button>
|
||||||
<Show
|
<Show
|
||||||
when={!confirmDeleteOpen() || isChildNote()}
|
when={!confirmDeleteOpen() || isChildNote()}
|
||||||
fallback={
|
fallback={
|
||||||
|
|||||||
Reference in New Issue
Block a user