Merge branch 'dev'
This commit is contained in:
commit
68ea2fe548
|
|
@ -744,19 +744,31 @@ class FolkCampaignPlanner extends HTMLElement {
|
|||
}
|
||||
case 'thread': {
|
||||
const d = node.data as ThreadNodeData;
|
||||
const threads = this.localFirstClient?.listThreads() || [];
|
||||
const threadOptions = threads.length === 0
|
||||
? '<option value="">No threads yet</option>'
|
||||
: [`<option value="">— Select a thread —</option>`,
|
||||
...threads.map(t =>
|
||||
`<option value="${t.id}" ${d.threadId === t.id ? 'selected' : ''}>${esc(t.title || 'Untitled')} (${t.tweets.length} tweets)</option>`
|
||||
)].join('');
|
||||
const selectedThread = d.threadId ? threads.find(t => t.id === d.threadId) : null;
|
||||
const previewText = selectedThread?.tweets[0]
|
||||
? esc(selectedThread.tweets[0].substring(0, 120)) + (selectedThread.tweets[0].length > 120 ? '...' : '')
|
||||
: '';
|
||||
body = `
|
||||
<div class="cp-icp-body">
|
||||
<label>Title</label>
|
||||
<input data-field="label" value="${esc(d.label)}" readonly/>
|
||||
<label>Thread</label>
|
||||
<select data-field="threadId" data-thread-picker="true">${threadOptions}</select>
|
||||
${previewText ? `<div style="font-size:11px;color:var(--text-secondary,#94a3b8);margin:4px 0 8px;line-height:1.4">${previewText}</div>` : ''}
|
||||
<label>Tweet Count</label>
|
||||
<input data-field="tweetCount" type="number" value="${d.tweetCount}"/>
|
||||
<input data-field="tweetCount" type="number" value="${d.tweetCount}" readonly/>
|
||||
<label>Status</label>
|
||||
<select data-field="status">
|
||||
<option value="draft" ${d.status === 'draft' ? 'selected' : ''}>Draft</option>
|
||||
<option value="ready" ${d.status === 'ready' ? 'selected' : ''}>Ready</option>
|
||||
<option value="published" ${d.status === 'published' ? 'selected' : ''}>Published</option>
|
||||
</select>
|
||||
<button class="cp-btn" data-action="open-thread" style="margin-top:8px;width:100%">Open in Thread Editor</button>
|
||||
<button class="cp-btn" data-action="open-thread" style="margin-top:8px;width:100%" ${!d.threadId ? 'disabled' : ''}>Open in Thread Editor</button>
|
||||
</div>`;
|
||||
break;
|
||||
}
|
||||
|
|
@ -808,7 +820,27 @@ class FolkCampaignPlanner extends HTMLElement {
|
|||
const field = el.getAttribute('data-field')!;
|
||||
const handler = () => {
|
||||
const val = (el as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement).value;
|
||||
if (field === 'hashtags') {
|
||||
if (field === 'threadId' && (el as HTMLElement).hasAttribute('data-thread-picker')) {
|
||||
const d = node.data as ThreadNodeData;
|
||||
if (val) {
|
||||
const thread = this.localFirstClient?.listThreads().find(t => t.id === val);
|
||||
if (thread) {
|
||||
d.threadId = thread.id;
|
||||
d.label = thread.title || 'Untitled';
|
||||
d.tweetCount = thread.tweets.length;
|
||||
d.preview = thread.tweets[0]?.substring(0, 120) || '';
|
||||
d.status = 'draft';
|
||||
}
|
||||
} else {
|
||||
d.threadId = '';
|
||||
d.label = 'New Thread';
|
||||
d.tweetCount = 0;
|
||||
d.preview = '';
|
||||
}
|
||||
this.scheduleSave();
|
||||
this.drawCanvasContent();
|
||||
return;
|
||||
} else if (field === 'hashtags') {
|
||||
(node.data as PostNodeData).hashtags = val.split(',').map(h => h.trim()).filter(Boolean);
|
||||
} else if (field === 'tweetCount') {
|
||||
(node.data as ThreadNodeData).tweetCount = parseInt(val, 10) || 0;
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@
|
|||
* space — space slug (default "demo")
|
||||
*/
|
||||
|
||||
import { CAMPAIGN_NODE_CATALOG } from '../schemas';
|
||||
import { CAMPAIGN_NODE_CATALOG, socialsDocId } from '../schemas';
|
||||
import type {
|
||||
CampaignWorkflowNodeDef,
|
||||
CampaignWorkflowNodeCategory,
|
||||
CampaignWorkflowNode,
|
||||
CampaignWorkflowEdge,
|
||||
CampaignWorkflow,
|
||||
ThreadData,
|
||||
SocialsDoc,
|
||||
} from '../schemas';
|
||||
|
||||
// ── Constants ──
|
||||
|
|
@ -125,6 +127,10 @@ class FolkCampaignWorkflow extends HTMLElement {
|
|||
// Execution log
|
||||
private execLog: { nodeId: string; status: string; message: string; durationMs: number }[] = [];
|
||||
|
||||
// Thread cache (for publish-thread node picker)
|
||||
private cachedThreads: ThreadData[] = [];
|
||||
private threadsLoaded = false;
|
||||
|
||||
// Bound listeners
|
||||
private _boundPointerMove: ((e: PointerEvent) => void) | null = null;
|
||||
private _boundPointerUp: ((e: PointerEvent) => void) | null = null;
|
||||
|
|
@ -172,6 +178,20 @@ class FolkCampaignWorkflow extends HTMLElement {
|
|||
requestAnimationFrame(() => this.fitView());
|
||||
}
|
||||
|
||||
private loadThreads() {
|
||||
if (this.threadsLoaded) return;
|
||||
this.threadsLoaded = true;
|
||||
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||
if (!runtime?.isInitialized) return;
|
||||
try {
|
||||
const docId = socialsDocId(this.space);
|
||||
const doc = runtime.documents?.get(docId) as SocialsDoc | undefined;
|
||||
if (doc?.threads) {
|
||||
this.cachedThreads = Object.values(doc.threads).sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
}
|
||||
} catch { /* offline runtime not available */ }
|
||||
}
|
||||
|
||||
private loadWorkflow(wf: CampaignWorkflow) {
|
||||
this.currentWorkflowId = wf.id;
|
||||
this.workflowName = wf.name;
|
||||
|
|
@ -480,8 +500,28 @@ class FolkCampaignWorkflow extends HTMLElement {
|
|||
const def = getNodeDef(node.type);
|
||||
if (!def) return '';
|
||||
|
||||
// Lazy-load threads when configuring a publish-thread node
|
||||
if (node.type === 'publish-thread') this.loadThreads();
|
||||
|
||||
const fieldsHtml = def.configSchema.map(field => {
|
||||
const val = node.config[field.key] ?? '';
|
||||
|
||||
// Replace threadId text input with a select dropdown for publish-thread nodes
|
||||
if (field.key === 'threadId' && node.type === 'publish-thread') {
|
||||
const threads = this.cachedThreads;
|
||||
const threadOptions = threads.length === 0
|
||||
? '<option value="">No threads yet</option>'
|
||||
: [`<option value="">— Select a thread —</option>`,
|
||||
...threads.map(t =>
|
||||
`<option value="${t.id}" ${val === t.id ? 'selected' : ''}>${esc(t.title || 'Untitled')} (${t.tweets.length} tweets)</option>`
|
||||
)].join('');
|
||||
return `
|
||||
<div class="cw-config__field">
|
||||
<label>${esc(field.label)}</label>
|
||||
<select data-config-key="threadId" id="config-thread-picker">${threadOptions}</select>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (field.type === 'select') {
|
||||
const options = (field.options || []).map(o =>
|
||||
`<option value="${o}" ${val === o ? 'selected' : ''}>${o}</option>`
|
||||
|
|
@ -855,6 +895,28 @@ class FolkCampaignWorkflow extends HTMLElement {
|
|||
if (this.selectedNodeId) this.deleteNode(this.selectedNodeId);
|
||||
});
|
||||
|
||||
this.shadow.getElementById('config-thread-picker')?.addEventListener('change', (e) => {
|
||||
const node = this.nodes.find(n => n.id === this.selectedNodeId);
|
||||
if (!node) return;
|
||||
const threadId = (e.target as HTMLSelectElement).value;
|
||||
node.config.threadId = threadId;
|
||||
if (threadId) {
|
||||
const thread = this.cachedThreads.find(t => t.id === threadId);
|
||||
if (thread) {
|
||||
node.config.threadContent = thread.tweets.join('\n---\n');
|
||||
node.label = thread.title || 'Publish Thread';
|
||||
// Update the threadContent textarea if visible
|
||||
const ta = this.shadow.querySelector('[data-config-key="threadContent"]') as HTMLTextAreaElement | null;
|
||||
if (ta) ta.value = node.config.threadContent as string;
|
||||
// Update label input
|
||||
const li = this.shadow.getElementById('config-label') as HTMLInputElement | null;
|
||||
if (li) li.value = node.label;
|
||||
this.drawCanvasContent();
|
||||
}
|
||||
}
|
||||
this.scheduleSave();
|
||||
});
|
||||
|
||||
this.shadow.getElementById('config-open-thread')?.addEventListener('click', () => {
|
||||
const node = this.nodes.find(n => n.id === this.selectedNodeId);
|
||||
if (!node) return;
|
||||
|
|
|
|||
|
|
@ -959,9 +959,15 @@ routes.post("/api/campaign-workflows/:id/run", async (c) => {
|
|||
}
|
||||
case 'publish-thread': {
|
||||
if (!postizConfig) throw new Error('Postiz not configured');
|
||||
const threadContent = cfg.threadContent as string || '';
|
||||
let threadContent = cfg.threadContent as string || '';
|
||||
if (!threadContent && cfg.threadId) {
|
||||
const linkedThread = doc.threads?.[cfg.threadId as string];
|
||||
if (linkedThread?.tweets?.length) {
|
||||
threadContent = linkedThread.tweets.join('\n---\n');
|
||||
}
|
||||
}
|
||||
const tweets = threadContent.split(/\n---\n/).map(s => s.trim()).filter(Boolean);
|
||||
if (tweets.length === 0) throw new Error('No thread content');
|
||||
if (tweets.length === 0) throw new Error('No thread content or thread ID provided');
|
||||
const integrations = await getIntegrations(postizConfig);
|
||||
const platformName = (cfg.platform as string || '').toLowerCase();
|
||||
const match = (integrations || []).find((i: any) =>
|
||||
|
|
|
|||
Loading…
Reference in New Issue