Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-20 14:28:30 -07:00
commit 2be38e7fee
5 changed files with 474 additions and 18 deletions

View File

@ -287,6 +287,7 @@ export class FolkThreadBuilder extends HTMLElement {
<button data-platform="plain">📄 Plain Text</button>
</div>
</div>
<button class="btn btn--postiz" id="ro-send-postiz">Send to Postiz</button>
</div>
<div class="ro-cta">
<a href="${this.basePath}thread-editor" class="btn btn--success">Create Your Own Thread</a>
@ -322,6 +323,7 @@ export class FolkThreadBuilder extends HTMLElement {
<button data-platform="plain">📄 Plain Text</button>
</div>
</div>
<button class="btn btn--postiz" id="thread-send-postiz">Send to Postiz</button>
</div>
</div>
<div class="drafts-area">
@ -838,6 +840,29 @@ export class FolkThreadBuilder extends HTMLElement {
});
});
// Send to Postiz
const postizBtn = sr.getElementById('thread-send-postiz');
postizBtn?.addEventListener('click', async () => {
const tweets = textarea.value.split(/\n---\n/).map(t => t.trim()).filter(Boolean);
if (!tweets.length) { postizBtn.textContent = 'No content'; setTimeout(() => { postizBtn.textContent = 'Send to Postiz'; }, 2000); return; }
postizBtn.textContent = 'Sending...';
(postizBtn as HTMLButtonElement).disabled = true;
try {
const res = await fetch(`/${this._space}/rsocials/api/postiz/threads`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tweets, type: 'draft' }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed');
postizBtn.textContent = 'Sent!';
setTimeout(() => { postizBtn.textContent = 'Send to Postiz'; (postizBtn as HTMLButtonElement).disabled = false; }, 3000);
} catch (err: any) {
postizBtn.textContent = err.message || 'Error';
setTimeout(() => { postizBtn.textContent = 'Send to Postiz'; (postizBtn as HTMLButtonElement).disabled = false; }, 3000);
}
});
// Per-tweet image operations (event delegation)
preview?.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
@ -969,6 +994,30 @@ export class FolkThreadBuilder extends HTMLElement {
if (exportMenu) exportMenu.hidden = true;
});
});
// Send to Postiz (readonly)
const roPostizBtn = sr.getElementById('ro-send-postiz');
roPostizBtn?.addEventListener('click', async () => {
if (!t) return;
roPostizBtn.textContent = 'Sending...';
(roPostizBtn as HTMLButtonElement).disabled = true;
try {
const res = await fetch(`/${this._space}/rsocials/api/postiz/threads`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tweets: t.tweets, type: 'draft' }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed');
this.showToast('Thread sent to Postiz as draft!');
roPostizBtn.textContent = 'Sent!';
setTimeout(() => { roPostizBtn.textContent = 'Send to Postiz'; (roPostizBtn as HTMLButtonElement).disabled = false; }, 3000);
} catch (err: any) {
this.showToast(err.message || 'Failed to send');
roPostizBtn.textContent = 'Error';
setTimeout(() => { roPostizBtn.textContent = 'Send to Postiz'; (roPostizBtn as HTMLButtonElement).disabled = false; }, 3000);
}
});
}
private showToast(msg: string) {
@ -991,6 +1040,8 @@ export class FolkThreadBuilder extends HTMLElement {
.btn--outline:hover { border-color: #6366f1; color: #c4b5fd; }
.btn--success { background: #10b981; color: white; }
.btn--success:hover { background: #34d399; }
.btn--postiz { background: #f97316; color: white; }
.btn--postiz:hover { background: #fb923c; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.tweet-card {

View File

@ -6,13 +6,25 @@
*/
import { socialsSchema, socialsDocId } from '../schemas';
import type { SocialsDoc, ThreadData } from '../schemas';
import type { SocialsDoc, ThreadData, Campaign, CampaignPost } from '../schemas';
import type { DocumentId } from '../../../shared/local-first/document';
import { DEMO_FEED } from '../lib/types';
interface DraftPostCard {
id: string;
campaignId: string;
campaignTitle: string;
platform: string;
content: string;
scheduledAt: string;
status: string;
hashtags: string[];
}
export class FolkThreadGallery extends HTMLElement {
private _space = 'demo';
private _threads: ThreadData[] = [];
private _draftPosts: DraftPostCard[] = [];
private _offlineUnsub: (() => void) | null = null;
private _isDemoFallback = false;
@ -79,6 +91,28 @@ export class FolkThreadGallery extends HTMLElement {
if (!doc?.threads) return;
this._isDemoFallback = false;
this._threads = Object.values(doc.threads).sort((a, b) => b.updatedAt - a.updatedAt);
// Extract draft/scheduled posts from campaigns
this._draftPosts = [];
if (doc.campaigns) {
for (const campaign of Object.values(doc.campaigns)) {
for (const post of campaign.posts || []) {
if (post.status === 'draft' || post.status === 'scheduled') {
this._draftPosts.push({
id: post.id,
campaignId: campaign.id,
campaignTitle: campaign.title,
platform: post.platform,
content: post.content,
scheduledAt: post.scheduledAt,
status: post.status,
hashtags: post.hashtags || [],
});
}
}
}
}
this.render();
}
@ -94,17 +128,45 @@ export class FolkThreadGallery extends HTMLElement {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
private platformIcon(platform: string): string {
const icons: Record<string, string> = {
x: '𝕏', twitter: '𝕏', linkedin: '💼', instagram: '📷',
threads: '🧵', bluesky: '🦋', youtube: '📹', newsletter: '📧',
};
return icons[platform.toLowerCase()] || '📱';
}
private render() {
if (!this.shadowRoot) return;
const space = this._space;
const threads = this._threads;
const drafts = this._draftPosts;
const cardsHTML = threads.length === 0
const threadCardsHTML = threads.length === 0 && drafts.length === 0
? `<div class="empty">
<p>No threads yet. Create your first thread!</p>
<p>No posts or threads yet. Create your first thread!</p>
<a href="${this.basePath}thread-editor" class="btn btn--success">Create Thread</a>
</div>`
: `<div class="grid">
${drafts.map(p => {
const preview = this.esc(p.content.substring(0, 200));
const schedDate = p.scheduledAt ? new Date(p.scheduledAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '';
const statusBadge = p.status === 'scheduled'
? '<span class="badge badge--scheduled">Scheduled</span>'
: '<span class="badge badge--draft">Draft</span>';
return `<div class="card card--draft">
<div class="card__badges">
${statusBadge}
<span class="badge badge--campaign">${this.esc(p.campaignTitle)}</span>
</div>
<h3 class="card__title">${this.platformIcon(p.platform)} ${this.esc(p.platform)} Post</h3>
<p class="card__preview">${preview}</p>
<div class="card__meta">
${p.hashtags.length ? `<span>${p.hashtags.slice(0, 3).join(' ')}</span>` : ''}
${schedDate ? `<span>${schedDate}</span>` : ''}
</div>
</div>`;
}).join('')}
${threads.map(t => {
const initial = (t.name || '?').charAt(0).toUpperCase();
const preview = this.esc((t.tweets[0] || '').substring(0, 200));
@ -155,6 +217,15 @@ export class FolkThreadGallery extends HTMLElement {
text-decoration: none; color: inherit;
}
.card:hover { border-color: var(--rs-primary); transform: translateY(-2px); }
.card--draft { border-left: 3px solid #f59e0b; }
.card__badges { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.badge {
font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
padding: 0.15rem 0.5rem; border-radius: 4px;
}
.badge--draft { background: rgba(245,158,11,0.15); color: #f59e0b; }
.badge--scheduled { background: rgba(59,130,246,0.15); color: #60a5fa; }
.badge--campaign { background: rgba(99,102,241,0.15); color: #a5b4fc; }
.card__title { font-size: 1rem; font-weight: 700; color: var(--rs-text-primary, #f1f5f9); margin: 0; line-height: 1.3; }
.card__preview {
font-size: 0.85rem; color: var(--rs-text-secondary, #94a3b8); line-height: 1.5;
@ -172,10 +243,10 @@ export class FolkThreadGallery extends HTMLElement {
</style>
<div class="gallery">
<div class="header">
<h1>Threads</h1>
<h1>Posts & Threads</h1>
<a href="${this.basePath}thread-editor" class="btn btn--primary">New Thread</a>
</div>
${cardsHTML}
${threadCardsHTML}
</div>
`;
}

View File

@ -0,0 +1,98 @@
/**
* Postiz API client reads per-space credentials from module settings
* and forwards authenticated requests to the Postiz public API.
*
* Follows the same pattern as listmonk-proxy.ts.
*/
import { loadCommunity, getDocumentData } from "../../../server/community-store";
export interface PostizConfig {
url: string;
apiKey: string;
}
/** Read Postiz credentials from the space's module settings. */
export async function getPostizConfig(spaceSlug: string): Promise<PostizConfig | null> {
await loadCommunity(spaceSlug);
const data = getDocumentData(spaceSlug);
if (!data) return null;
const settings = data.meta.moduleSettings?.rsocials;
if (!settings) return null;
const url = settings.postizUrl as string | undefined;
const apiKey = settings.postizApiKey as string | undefined;
if (!url || !apiKey) return null;
return { url: url.replace(/\/+$/, ''), apiKey };
}
/** Make an authenticated request to the Postiz API. */
export async function postizFetch(
config: PostizConfig,
path: string,
opts: RequestInit = {},
): Promise<Response> {
const headers = new Headers(opts.headers);
headers.set("Authorization", `Bearer ${config.apiKey}`);
if (!headers.has("Content-Type") && opts.body) {
headers.set("Content-Type", "application/json");
}
return fetch(`${config.url}${path}`, { ...opts, headers });
}
/** GET /public/v1/integrations — list connected social channels. */
export async function getIntegrations(config: PostizConfig) {
const res = await postizFetch(config, "/public/v1/integrations");
if (!res.ok) throw new Error(`Postiz integrations error: ${res.status}`);
return res.json();
}
/** POST /public/v1/posts — create a single post (draft, schedule, or now). */
export async function createPost(
config: PostizConfig,
payload: {
content: string;
integrationIds: string[];
type: 'draft' | 'schedule' | 'now';
scheduledAt?: string;
group?: string;
},
) {
const res = await postizFetch(config, "/public/v1/posts", {
method: "POST",
body: JSON.stringify(payload),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`Postiz createPost error: ${res.status} ${text}`);
}
return res.json();
}
/** Create a thread — sends multiple grouped posts sharing a group ID. */
export async function createThread(
config: PostizConfig,
tweets: string[],
opts: {
integrationIds: string[];
type: 'draft' | 'schedule' | 'now';
scheduledAt?: string;
},
) {
const group = `thread-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
const results = [];
for (const content of tweets) {
const result = await createPost(config, {
content,
integrationIds: opts.integrationIds,
type: opts.type,
scheduledAt: opts.scheduledAt,
group,
});
results.push(result);
}
return { group, posts: results };
}

View File

@ -19,7 +19,7 @@ import type { RSpaceModule } from "../../shared/module";
import type { SyncServer } from "../../server/local-first/sync-server";
import { renderLanding } from "./landing";
import { MYCOFI_CAMPAIGN, buildDemoCampaignFlow, buildDemoCampaignWorkflow } from "./campaign-data";
import { socialsSchema, socialsDocId, type SocialsDoc, type ThreadData, type CampaignWorkflow, type CampaignWorkflowNode, type CampaignWorkflowEdge } from "./schemas";
import { socialsSchema, socialsDocId, type SocialsDoc, type ThreadData, type CampaignWorkflow, type CampaignWorkflowNode, type CampaignWorkflowEdge, type PendingApproval } from "./schemas";
import {
generateImageFromPrompt,
downloadAndSaveImage,
@ -31,6 +31,7 @@ import {
} from "./lib/image-gen";
import { DEMO_FEED } from "./lib/types";
import { getListmonkConfig, listmonkFetch } from "./lib/listmonk-proxy";
import { getPostizConfig, getIntegrations, createPost, createThread } from "./lib/postiz-client";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import type { EncryptIDClaims } from "@encryptid/sdk/server";
import { resolveCallerRole, roleAtLeast } from "../../server/spaces";
@ -55,6 +56,7 @@ function ensureDoc(space: string): SocialsDoc {
d.campaignFlows = {};
d.activeFlowId = '';
d.campaignWorkflows = {};
d.pendingApprovals = {};
});
_syncServer!.setDoc(docId, doc);
}
@ -515,6 +517,125 @@ routes.put("/api/newsletter/campaigns/:id/status", async (c) => {
return c.json(data, res.status as any);
});
// ── Postiz API proxy routes ──
routes.get("/api/postiz/status", async (c) => {
const space = c.req.param("space") || "demo";
const config = await getPostizConfig(space);
return c.json({ configured: !!config });
});
routes.get("/api/postiz/integrations", async (c) => {
const space = c.req.param("space") || "demo";
const config = await getPostizConfig(space);
if (!config) return c.json({ error: "Postiz not configured" }, 404);
try {
const data = await getIntegrations(config);
return c.json(data);
} catch (err: any) {
return c.json({ error: err.message }, 502);
}
});
routes.post("/api/postiz/posts", async (c) => {
const space = c.req.param("space") || "demo";
const config = await getPostizConfig(space);
if (!config) return c.json({ error: "Postiz not configured" }, 404);
const body = await c.req.json();
const { content, integrationIds, type, scheduledAt } = body;
if (!content || !integrationIds?.length) {
return c.json({ error: "content and integrationIds are required" }, 400);
}
try {
const result = await createPost(config, {
content,
integrationIds,
type: type || 'draft',
scheduledAt,
});
return c.json(result);
} catch (err: any) {
return c.json({ error: err.message }, 502);
}
});
routes.post("/api/postiz/threads", async (c) => {
const space = c.req.param("space") || "demo";
const config = await getPostizConfig(space);
if (!config) return c.json({ error: "Postiz not configured" }, 404);
const body = await c.req.json();
const { tweets, integrationIds, type, scheduledAt } = body;
if (!tweets?.length) {
return c.json({ error: "tweets array is required" }, 400);
}
// If no integrationIds provided, try to auto-detect from configured integrations
let ids = integrationIds;
if (!ids?.length) {
try {
const integrations = await getIntegrations(config);
ids = (integrations || []).map((i: any) => i.id).slice(0, 1);
} catch { /* fall through */ }
}
if (!ids?.length) {
return c.json({ error: "No integrationIds provided and no integrations found" }, 400);
}
try {
const result = await createThread(config, tweets, {
integrationIds: ids,
type: type || 'draft',
scheduledAt,
});
return c.json(result);
} catch (err: any) {
return c.json({ error: err.message }, 502);
}
});
// ── Approval queue routes ──
routes.get("/api/approvals", (c) => {
const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space;
const doc = ensureDoc(dataSpace);
const approvals = Object.values(doc.pendingApprovals || {})
.filter(a => a.status === 'pending')
.sort((a, b) => b.createdAt - a.createdAt);
return c.json(approvals);
});
routes.post("/api/approvals/:id/resolve", async (c) => {
const space = c.req.param("space") || "demo";
const dataSpace = c.get("effectiveSpace") || space;
const approvalId = c.req.param("id");
const body = await c.req.json();
const action = body.action as 'approve' | 'reject';
if (!action || !['approve', 'reject'].includes(action)) {
return c.json({ error: "action must be 'approve' or 'reject'" }, 400);
}
const docId = socialsDocId(dataSpace);
const doc = ensureDoc(dataSpace);
const approval = doc.pendingApprovals?.[approvalId];
if (!approval) return c.json({ error: "Approval not found" }, 404);
if (approval.status !== 'pending') return c.json({ error: "Approval already resolved" }, 409);
_syncServer!.changeDoc<SocialsDoc>(docId, `resolve approval ${approvalId}`, (d) => {
const a = d.pendingApprovals[approvalId];
if (!a) return;
a.status = action === 'approve' ? 'approved' : 'rejected';
a.resolvedAt = Date.now();
});
return c.json({ ok: true, status: action === 'approve' ? 'approved' : 'rejected' });
});
// ── AI Campaign Generator ──
routes.post("/api/campaign/generate", async (c) => {
@ -746,18 +867,115 @@ routes.post("/api/campaign-workflows/:id/run", async (c) => {
const wf = doc.campaignWorkflows?.[id];
if (!wf) return c.json({ error: "Campaign workflow not found" }, 404);
// Stub execution — topological walk, each node returns stub success
// Execute workflow — topological walk with real Postiz integration
const results: { nodeId: string; status: string; message: string; durationMs: number }[] = [];
const sorted = topologicalSortCampaign(wf.nodes, wf.edges);
const postizConfig = await getPostizConfig(dataSpace);
let hasError = false;
let paused = false;
for (const node of sorted) {
if (paused) break;
const start = Date.now();
results.push({
nodeId: node.id,
status: 'success',
message: `[stub] ${node.label} executed`,
durationMs: Date.now() - start,
});
const cfg = node.config || {};
try {
switch (node.type) {
case 'post-to-platform': {
if (!postizConfig) throw new Error('Postiz not configured');
const integrations = await getIntegrations(postizConfig);
const platformName = (cfg.platform as string || '').toLowerCase();
const match = (integrations || []).find((i: any) =>
i.name?.toLowerCase().includes(platformName) || i.providerIdentifier?.toLowerCase().includes(platformName)
);
const integrationIds = match ? [match.id] : (integrations || []).slice(0, 1).map((i: any) => i.id);
const content = (cfg.content as string || '') + (cfg.hashtags ? '\n' + cfg.hashtags : '');
await createPost(postizConfig, { content, integrationIds, type: 'draft' });
results.push({ nodeId: node.id, status: 'success', message: `Draft created on ${cfg.platform || 'default'}`, durationMs: Date.now() - start });
break;
}
case 'cross-post': {
if (!postizConfig) throw new Error('Postiz not configured');
const platforms = (cfg.platforms as string || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
const integrations = await getIntegrations(postizConfig);
const content = (cfg.content as string || '');
const posted: string[] = [];
for (const plat of platforms) {
const match = (integrations || []).find((i: any) =>
i.name?.toLowerCase().includes(plat) || i.providerIdentifier?.toLowerCase().includes(plat)
);
if (match) {
await createPost(postizConfig, { content, integrationIds: [match.id], type: 'draft' });
posted.push(plat);
}
}
results.push({ nodeId: node.id, status: 'success', message: `Cross-posted draft to: ${posted.join(', ') || 'none matched'}`, durationMs: Date.now() - start });
break;
}
case 'publish-thread': {
if (!postizConfig) throw new Error('Postiz not configured');
const threadContent = cfg.threadContent as string || '';
const tweets = threadContent.split(/\n---\n/).map(s => s.trim()).filter(Boolean);
if (tweets.length === 0) throw new Error('No thread content');
const integrations = await getIntegrations(postizConfig);
const platformName = (cfg.platform as string || '').toLowerCase();
const match = (integrations || []).find((i: any) =>
i.name?.toLowerCase().includes(platformName) || i.providerIdentifier?.toLowerCase().includes(platformName)
);
const integrationIds = match ? [match.id] : (integrations || []).slice(0, 1).map((i: any) => i.id);
await createThread(postizConfig, tweets, { integrationIds, type: 'draft' });
results.push({ nodeId: node.id, status: 'success', message: `Thread draft created (${tweets.length} tweets)`, durationMs: Date.now() - start });
break;
}
case 'send-newsletter': {
// Newsletter sending via Listmonk — log only for now
results.push({ nodeId: node.id, status: 'success', message: `[listmonk] Newsletter node logged (subject: ${cfg.subject || 'N/A'})`, durationMs: Date.now() - start });
break;
}
case 'post-webhook': {
const webhookUrl = cfg.url as string;
if (!webhookUrl) throw new Error('No webhook URL configured');
const bodyTemplate = cfg.bodyTemplate as string || '{}';
const res = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: bodyTemplate,
});
results.push({ nodeId: node.id, status: res.ok ? 'success' : 'error', message: `Webhook ${res.status}`, durationMs: Date.now() - start });
if (!res.ok) hasError = true;
break;
}
case 'wait-approval': {
// Create a pending approval and pause execution
const docId = socialsDocId(dataSpace);
const approvalId = `apr-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`;
_syncServer!.changeDoc<SocialsDoc>(docId, `create approval for ${node.id}`, (d) => {
if (!d.pendingApprovals) (d as any).pendingApprovals = {};
d.pendingApprovals[approvalId] = {
id: approvalId,
workflowId: id,
nodeId: node.id,
message: (cfg.message as string) || 'Approval required',
approver: (cfg.approver as string) || '',
status: 'pending',
createdAt: Date.now(),
resolvedAt: null,
};
});
results.push({ nodeId: node.id, status: 'paused', message: `Awaiting approval (${approvalId})`, durationMs: Date.now() - start });
paused = true;
break;
}
default: {
// Triggers, conditions, delays — pass through
results.push({ nodeId: node.id, status: 'success', message: `${node.label} passed`, durationMs: Date.now() - start });
break;
}
}
} catch (err: any) {
hasError = true;
results.push({ nodeId: node.id, status: 'error', message: err.message, durationMs: Date.now() - start });
}
}
// Update run metadata
@ -766,11 +984,11 @@ routes.post("/api/campaign-workflows/:id/run", async (c) => {
const w = d.campaignWorkflows[id];
if (!w) return;
w.lastRunAt = Date.now();
w.lastRunStatus = 'success';
w.lastRunStatus = hasError ? 'error' : 'success';
w.runCount = (w.runCount || 0) + 1;
});
return c.json({ results });
return c.json({ results, paused });
});
// POST /api/campaign-workflows/webhook/:hookId — external webhook trigger
@ -1154,6 +1372,8 @@ export const socialsModule: RSpaceModule = {
{ key: 'listmonkUrl', label: 'Listmonk URL', type: 'string', description: 'Base URL of your Listmonk instance (e.g. https://newsletter.example.com)' },
{ key: 'listmonkUser', label: 'Listmonk Username', type: 'string', description: 'API username for Listmonk' },
{ key: 'listmonkPassword', label: 'Listmonk Password', type: 'password', description: 'API password for Listmonk' },
{ key: 'postizUrl', label: 'Postiz URL', type: 'string', description: 'Base URL of your Postiz instance (e.g. https://demo.rsocials.online)' },
{ key: 'postizApiKey', label: 'Postiz API Key', type: 'password', description: 'API key from Postiz Settings > Developers' },
],
standaloneDomain: "rsocials.online",
landingPage: renderLanding,

View File

@ -359,6 +359,19 @@ export const CAMPAIGN_NODE_CATALOG: CampaignWorkflowNodeDef[] = [
},
];
// ── Approval queue types ──
export interface PendingApproval {
id: string;
workflowId: string;
nodeId: string;
message: string;
approver: string;
status: 'pending' | 'approved' | 'rejected';
createdAt: number;
resolvedAt: number | null;
}
// ── Document root ──
export interface SocialsDoc {
@ -374,6 +387,7 @@ export interface SocialsDoc {
campaignFlows: Record<string, CampaignFlow>;
activeFlowId: string;
campaignWorkflows: Record<string, CampaignWorkflow>;
pendingApprovals: Record<string, PendingApproval>;
}
// ── Schema registration ──
@ -381,12 +395,12 @@ export interface SocialsDoc {
export const socialsSchema: DocSchema<SocialsDoc> = {
module: 'socials',
collection: 'data',
version: 4,
version: 5,
init: (): SocialsDoc => ({
meta: {
module: 'socials',
collection: 'data',
version: 4,
version: 5,
spaceSlug: '',
createdAt: Date.now(),
},
@ -395,12 +409,14 @@ export const socialsSchema: DocSchema<SocialsDoc> = {
campaignFlows: {},
activeFlowId: '',
campaignWorkflows: {},
pendingApprovals: {},
}),
migrate: (doc: SocialsDoc, _fromVersion: number): SocialsDoc => {
if (!doc.campaignFlows) (doc as any).campaignFlows = {};
if (!doc.activeFlowId) (doc as any).activeFlowId = '';
if (!doc.campaignWorkflows) (doc as any).campaignWorkflows = {};
if (doc.meta) doc.meta.version = 4;
if (!doc.pendingApprovals) (doc as any).pendingApprovals = {};
if (doc.meta) doc.meta.version = 5;
return doc;
},
};