diff --git a/check_schema.ts b/check_schema.ts new file mode 100644 index 0000000..5436d20 --- /dev/null +++ b/check_schema.ts @@ -0,0 +1,15 @@ +import PocketBase from 'pocketbase'; + +const pb = new PocketBase('https://pocketbase.ccllc.pro'); + +async function check() { + try { + await pb.admins.authWithPassword("timothy@ccllc.pro", 'pass'); + const res = await pb.collection('Job_Info_Prod').getList(1, 1); + console.log("Got item:", res.items[0]); + } catch (e) { + console.error("Auth / Fetch Error", e); + } +} + +check(); diff --git a/src/components/JobFileSelector.tsx b/src/components/JobFileSelector.tsx new file mode 100644 index 0000000..88c3983 --- /dev/null +++ b/src/components/JobFileSelector.tsx @@ -0,0 +1,183 @@ +import { createSignal, createResource, Show, For, onMount } from "solid-js"; +import { pb } from "@/lib/pocketbase"; +import { store, activeNoteId } from "@/store"; +import { Search, File as FileIcon, Loader2, FolderOpen } from "lucide-solid"; + +interface JobFileSelectorProps { + onSelectFile: (jobId: string, filename: string) => void; +} + +export const JobFileSelector = (props: JobFileSelectorProps) => { + const [query, setQuery] = createSignal(""); + const [selectedJobId, setSelectedJobId] = createSignal(null); + + // Auto-detect job ID from active note tags + onMount(() => { + const currentNoteId = activeNoteId(); + if (currentNoteId) { + const currentNote = store.notes.find(n => n.id === currentNoteId); + if (currentNote) { + const jobIdTag = currentNote.tags.find(t => t.startsWith("job_id:")); + if (jobIdTag) { + const id = jobIdTag.split(":")[1]; + if (id) { + setSelectedJobId(id); + } + } + } + } + }); + + // Query Jobs + const [jobs] = createResource(query, async (q) => { + if (!q.trim()) return []; + try { + const res = await pb.collection('Job_Info_Prod').getList(1, 10, { + filter: `Job_Number ~ "${q}" || Job_Name ~ "${q}"`, + sort: '-created', + requestKey: null // Allow concurrent overlapping requests without auto-cancelling if needed, or leave it + }); + return res.items; + } catch (e) { + return []; + } + }); + + // Fetch details for the selected job to list files + const [selectedJob] = createResource(selectedJobId, async (id) => { + if (!id) return null; + try { + const data = await pb.collection('Job_Info_Prod').getOne(id); + console.log("Job File Selector Selected Job Data:", data); + return data; + } catch (e) { + console.error("Job File Selector Error Fetching Job:", e); + return null; + } + }); + + const categories = ["plans", "EST_and_CO", "Addendums", "files"] as const; + + return ( +
+ +
+
+ +
+
+

Embed a Job File

+

Search for a Prism job to browse its files.

+
+ +
+ + setQuery(e.currentTarget.value)} + /> +
+ + +
+ +
+
+ + 0}> +
+ + {(job) => ( + + )} + +
+
+
+
+ + + +
+ + Loading Job Files... +
+
+ + + {(jobRecord) => ( +
+
+
+

{jobRecord().Job_Name || "Unnamed Job"}

+ Job #{jobRecord().Job_Number || jobRecord().id} +
+ +
+ +
+ + {(category) => { + let files = jobRecord()[category]; + if (files === null || files === undefined || files === "") return null; + if (!Array.isArray(files)) files = [files]; + files = files.filter(Boolean); + + if (files.length === 0) return null; + + return ( +
+

{category.replace(/_/g, " ")}

+
+ + {(filename) => ( + + )} + +
+
+ ); + }} +
+ + {/* Empty state if no files at all */} + { + const val = jobRecord()[c]; + return !val || (Array.isArray(val) && val.length === 0) || (typeof val === 'string' && val.trim() === ''); + })}> +
+ No files found for this job. +
+
+
+
+ )} +
+
+
+ ); +}; diff --git a/src/lib/extensions/file-viewer.ts b/src/lib/extensions/file-viewer.ts index 1aed48d..44d33a9 100644 --- a/src/lib/extensions/file-viewer.ts +++ b/src/lib/extensions/file-viewer.ts @@ -1,4 +1,6 @@ import { Node, mergeAttributes } from '@tiptap/core'; +import { render } from 'solid-js/web'; +import { JobFileSelector } from '@/components/JobFileSelector'; export interface FileViewerOptions { HTMLAttributes: Record; @@ -24,31 +26,16 @@ export const FileViewer = Node.create({ addOptions() { return { HTMLAttributes: { - class: 'file-viewer-node', + class: 'file-viewer-node group relative', }, }; }, addAttributes() { return { - src: { - default: null, - parseHTML: element => { - const iframe = element.querySelector('iframe'); - if (iframe) { - return iframe.getAttribute('src'); - } - return element.getAttribute('src'); - }, - renderHTML: attributes => { - if (!attributes.src) { - return {}; - } - return { - src: attributes.src, - }; - } - }, + jobId: { default: null }, + collectionId: { default: 'Job_Info_Prod' }, + filename: { default: null }, }; }, @@ -61,8 +48,12 @@ export const FileViewer = Node.create({ }, renderHTML({ HTMLAttributes }) { - const { src, ...rest } = HTMLAttributes; - return ['div', mergeAttributes(this.options.HTMLAttributes, rest, { 'data-type': 'file-viewer-wrapper', class: 'file-viewer-wrapper relative w-full my-4 rounded-xl overflow-hidden border border-border flex min-h-[400px] bg-muted/20' }), + const { jobId, collectionId, filename, ...rest } = HTMLAttributes; + let src = ''; + if (jobId && filename) { + src = `https://pocketbase.ccllc.pro/api/files/${collectionId}/${jobId}/${filename}`; + } + return ['div', mergeAttributes(this.options.HTMLAttributes, rest, { 'data-type': 'file-viewer-wrapper', class: 'file-viewer-wrapper relative w-full my-4 rounded-xl overflow-hidden border border-border flex min-h-[500px] bg-muted/20' }), ['iframe', { 'data-type': 'file-viewer', src: src, class: 'w-full h-full border-0 absolute inset-0' }] ]; }, @@ -72,15 +63,17 @@ export const FileViewer = Node.create({ const wrapper = document.createElement('div'); wrapper.classList.add('file-viewer-wrapper', 'relative', 'w-full', 'my-4', 'rounded-xl', 'overflow-hidden', 'border', 'border-border', 'flex', 'bg-muted/10', 'group'); - if (node.attrs.src) { - // Typical file viewer mode - wrapper.style.minHeight = '400px'; + const hasFile = node.attrs.jobId && node.attrs.filename; + + if (hasFile) { + // Viewer mode + wrapper.style.minHeight = '500px'; const iframe = document.createElement('iframe'); - iframe.src = node.attrs.src; + iframe.src = `https://pocketbase.ccllc.pro/api/files/${node.attrs.collectionId}/${node.attrs.jobId}/${node.attrs.filename}`; iframe.classList.add('w-full', 'h-full', 'border-0', 'absolute', 'inset-0'); - // Add a small drag handle / delete button overlay that appears on hover + // Delete button overlay const overlay = document.createElement('div'); overlay.classList.add('absolute', 'top-2', 'right-2', 'hidden', 'group-hover:flex', 'bg-background/80', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10'); @@ -100,64 +93,34 @@ export const FileViewer = Node.create({ overlay.appendChild(deleteBtn); wrapper.appendChild(iframe); wrapper.appendChild(overlay); - } else { - // Form mode - wrapper.classList.add('items-center', 'justify-center', 'flex-col', 'p-8', 'gap-4'); - const icon = document.createElement('div'); - // AppWindow Lucide Icon - icon.innerHTML = ''; - - const title = document.createElement('div'); - title.textContent = 'Embed File or Website'; - title.classList.add('font-medium', 'text-sm', 'text-muted-foreground'); - - const form = document.createElement('form'); - form.classList.add('flex', 'w-full', 'max-w-md', 'gap-2', 'relative', 'z-10'); - - const input = document.createElement('input'); - input.type = 'url'; - input.placeholder = 'https://...'; - input.classList.add('flex-1', 'bg-background', 'border', 'border-border', 'rounded-lg', 'px-3', 'py-1.5', 'text-sm', 'outline-none', 'focus:ring-1', 'focus:ring-primary/50'); - - const button = document.createElement('button'); - button.type = 'submit'; - button.textContent = 'Embed'; - button.classList.add('bg-primary', 'text-primary-foreground', 'px-3', 'py-1.5', 'rounded-lg', 'text-sm', 'font-medium', 'hover:bg-primary/90', 'cursor-pointer'); - - form.onsubmit = (e) => { - e.preventDefault(); - const url = input.value.trim(); - if (url && typeof getPos === 'function') { - editor.commands.command(({ tr }) => { - const pos = getPos(); - if (typeof pos === 'number') { - tr.setNodeMarkup(pos, undefined, { src: url }); - return true; - } - return false; - }); - } + return { + dom: wrapper, + stopEvent: () => true, + ignoreMutation: () => true, }; + } else { + render(() => JobFileSelector({ + onSelectFile: (jobId: string, filename: string) => { + if (typeof getPos === 'function') { + editor.commands.command(({ tr }) => { + const pos = getPos(); + if (typeof pos === 'number') { + tr.setNodeMarkup(pos, undefined, { jobId, collectionId: 'Job_Info_Prod', filename }); + return true; + } + return false; + }); + } + } + }), wrapper); - form.appendChild(input); - form.appendChild(button); - - wrapper.appendChild(icon); - wrapper.appendChild(title); - wrapper.appendChild(form); - - // Clicking wrapper focuses input - wrapper.onclick = () => { - input.focus(); + return { + dom: wrapper, + stopEvent: () => true, + ignoreMutation: () => true, }; } - - return { - dom: wrapper, - stopEvent: () => true, - ignoreMutation: () => true, - }; }; },