Skip to content

Commit 1a9d416

Browse files
committed
feat(slack): add manual post modal and draft pagination on Home
- Add manual post modal (text, platform, optional schedule time) - Add Schedule button to draft card opening dedicated schedule modal - Filter Home drafts to pending/generating/generated/scheduled only - Add offset-based pagination (10 per page) with prev/next buttons - Add all new UI strings to lexicon
1 parent 6a0eabd commit 1a9d416

4 files changed

Lines changed: 219 additions & 8 deletions

File tree

backend/api/routes/feedback.py

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
build_approval_modal,
2323
build_draft_card,
2424
build_generation_modal,
25+
build_manual_post_modal,
2526
build_schedule_modal,
2627
build_upload_modal,
2728
)
@@ -100,6 +101,29 @@ async def slack_interactions(
100101
)
101102
return Response(status_code=200)
102103

104+
if action_id == "action_open_manual_post_modal":
105+
modal_view = build_manual_post_modal()
106+
async with httpx.AsyncClient() as client:
107+
await client.post(
108+
"https://slack.com/api/views.open",
109+
headers=headers,
110+
json={"trigger_id": trigger_id, "view": modal_view},
111+
)
112+
return Response(status_code=200)
113+
114+
if action_id == "action_home_drafts_page":
115+
page_offset = int(action_value) if action_value.isdigit() else 0
116+
repo = DraftRepository(session)
117+
drafts = await repo.get_recent_drafts(limit=10, offset=page_offset)
118+
home_view = build_app_home(drafts=drafts, offset=page_offset)
119+
async with httpx.AsyncClient() as client:
120+
await client.post(
121+
"https://slack.com/api/views.publish",
122+
headers=headers,
123+
json={"user_id": user_id, "view": home_view},
124+
)
125+
return Response(status_code=200)
126+
103127
if action_id == "action_open_generation_modal":
104128
modal_view = build_generation_modal(channel_id=user_id)
105129
async with httpx.AsyncClient() as client:
@@ -243,7 +267,6 @@ async def slack_interactions(
243267
callback_id = view.get("callback_id")
244268
state_values = view.get("state", {}).get("values", {})
245269

246-
# --- СЦЕНАРІЙ 1: Генерація нового драфту ---
247270
# --- СЦЕНАРІЙ 1: Генерація нового драфту ---
248271
if callback_id == "modal_generate_draft":
249272
channel_id = view.get("private_metadata")
@@ -388,7 +411,81 @@ async def slack_interactions(
388411
status_code=200,
389412
)
390413

391-
# --- СЦЕНАРІЙ 3: Планування публікації ---
414+
# --- СЦЕНАРІЙ 3: Ручний пост ---
415+
elif callback_id == "modal_manual_post":
416+
content = (
417+
state_values.get("block_manual_content", {})
418+
.get("input_manual_content", {})
419+
.get("value", "")
420+
.strip()
421+
)
422+
423+
block_state = state_values.get("block_platform_select", {}).get(
424+
"input_platform_select", {}
425+
)
426+
selected_option = block_state.get("selected_option")
427+
platform_raw = selected_option.get("value", "telegram") if selected_option else "telegram"
428+
platform = Platform(platform_raw)
429+
430+
schedule_timestamp = (
431+
state_values.get("block_schedule_time", {})
432+
.get("input_schedule_time", {})
433+
.get("selected_date_time")
434+
)
435+
scheduled_at = (
436+
datetime.fromtimestamp(int(schedule_timestamp), tz=timezone.utc)
437+
if schedule_timestamp
438+
else None
439+
)
440+
441+
# Знаходимо або створюємо користувача
442+
user_query = await session.execute(select(User).where(User.username == user_id))
443+
db_user = user_query.scalar_one_or_none()
444+
if not db_user:
445+
db_user = User(username=user_id)
446+
session.add(db_user)
447+
await session.flush()
448+
449+
repo = DraftRepository(session)
450+
451+
if scheduled_at:
452+
new_draft = await repo.create(
453+
DraftCreate(topic=content[:80], platform=platform, user_id=db_user.id)
454+
)
455+
await repo.update(
456+
new_draft.id,
457+
DraftUpdate(
458+
content=content,
459+
status=DraftStatus.SCHEDULED,
460+
scheduled_at=scheduled_at,
461+
),
462+
)
463+
logger.info(
464+
"manual_post_scheduled",
465+
user_id=user_id,
466+
platform=platform,
467+
scheduled_at=scheduled_at.isoformat(),
468+
)
469+
else:
470+
new_draft = await repo.create(
471+
DraftCreate(topic=content[:80], platform=platform, user_id=db_user.id)
472+
)
473+
await repo.update(
474+
new_draft.id,
475+
DraftUpdate(content=content, status=DraftStatus.PUBLISHED),
476+
)
477+
await publish_post_task.kiq(
478+
post_id=str(new_draft.id), platform=platform.value, content=content
479+
)
480+
logger.info("manual_post_published", user_id=user_id, platform=platform)
481+
482+
return Response(
483+
content=json.dumps({"response_action": "clear"}),
484+
media_type="application/json",
485+
status_code=200,
486+
)
487+
488+
# --- СЦЕНАРІЙ 4: Планування публікації ---
392489
elif callback_id == "modal_schedule_draft":
393490
metadata_parts = view.get("private_metadata", "").split("|")
394491
draft_id = metadata_parts[0] if len(metadata_parts) > 0 else ""
@@ -432,7 +529,7 @@ async def slack_interactions(
432529
status_code=200,
433530
)
434531

435-
# --- СЦЕНАРІЙ 4: Завантаження гайдлайну ---
532+
# --- СЦЕНАРІЙ 5: Завантаження гайдлайну ---
436533
elif callback_id == "modal_upload_guideline":
437534
files = (
438535
state_values.get("block_file_upload", {})
@@ -490,7 +587,7 @@ async def slack_events(
490587
if hasattr(settings.SLACK_BOT_TOKEN, "get_secret_value")
491588
else settings.SLACK_BOT_TOKEN
492589
)
493-
home_view = build_app_home(drafts=recent_drafts)
590+
home_view = build_app_home(drafts=recent_drafts, offset=0)
494591
async with httpx.AsyncClient() as client:
495592
res = await client.post(
496593
"https://slack.com/api/views.publish",

backend/config/lexicon.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,24 @@
5050
"home_drafts_empty": "_Поки що немає жодного драфту._",
5151
"home_draft_card_text": "*{topic}*\nПлатформа: *{platform}* | Статус: {status_emoji} `{status}`",
5252
"home_draft_open_btn": "📝 Відкрити",
53+
"home_btn_next_page": "Наступні →",
54+
"home_btn_prev_page": "← Попередні",
5355
# --- Upload Modal ---
5456
"upload_modal_title": "База знань",
5557
"upload_modal_submit": "Завантажити",
5658
"upload_modal_input_label": "Оберіть файл (PDF/TXT)",
5759
# --- Upload Notifications ---
5860
"upload_success": "✅ Гайдлайн *{file_name}* успішно завантажено та векторизовано у базу знань!",
5961
"upload_failure": "❌ *Помилка* обробки гайдлайну *{file_name}*.\n\nДеталі:\n```{error_msg}```",
62+
# --- Manual Post Modal ---
63+
"home_btn_manual_post": "✏️ Написати пост",
64+
"manual_post_modal_title": "Новий пост вручну",
65+
"manual_post_modal_submit": "Опублікувати / Запланувати",
66+
"manual_post_modal_content_label": "Текст публікації",
67+
"manual_post_modal_platform_label": "Платформа",
68+
"manual_post_modal_schedule_label": "📅 Запланувати (UTC, необов'язково)",
69+
"manual_post_published": "✅ Пост опубліковано!",
70+
"manual_post_scheduled": "🕒 Пост заплановано на *{scheduled_at}* UTC!",
6071
# --- Schedule ---
6172
"btn_schedule": "🕒 Запланувати",
6273
"schedule_modal_title": "Планування публікації",

backend/repositories/draft_repository.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,18 @@ async def update(self, draft_id: int, draft_update: DraftUpdate) -> Draft | None
4343
return result.scalar_one_or_none()
4444

4545
async def get_recent_drafts(
46-
self, limit: int = 10, platform: str | None = None
46+
self, limit: int = 10, offset: int = 0, platform: str | None = None
4747
) -> list[Draft]:
4848
"""Витягує останні драфти (для Дашборду в Slack)"""
49-
stmt = select(Draft).order_by(Draft.updated_at.desc())
49+
active_statuses = ("pending", "generating", "generated", "scheduled")
50+
stmt = (
51+
select(Draft)
52+
.where(Draft.status.in_(active_statuses))
53+
.order_by(Draft.updated_at.desc())
54+
)
5055
if platform:
5156
stmt = stmt.where(Draft.platform == platform)
52-
stmt = stmt.limit(limit)
57+
stmt = stmt.limit(limit).offset(offset)
5358

5459
result = await self.session.execute(stmt)
5560
return list(result.scalars().all())

slack_app/utils/block_builder.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,74 @@ def build_generation_modal(channel_id: str) -> dict[str, Any]:
313313
}
314314

315315

316-
def build_app_home(drafts: list[Draft] | None = None) -> dict[str, Any]:
316+
def build_manual_post_modal() -> dict[str, Any]:
317+
"""Генерує модалку для ручного написання і публікації/планування поста."""
318+
return {
319+
"type": "modal",
320+
"callback_id": "modal_manual_post",
321+
"title": {"type": "plain_text", "text": SLACK_UI["manual_post_modal_title"], "emoji": True},
322+
"submit": {"type": "plain_text", "text": SLACK_UI["manual_post_modal_submit"], "emoji": True},
323+
"close": {"type": "plain_text", "text": SLACK_UI["modal_cancel"], "emoji": True},
324+
"blocks": [
325+
{
326+
"type": "input",
327+
"block_id": "block_manual_content",
328+
"element": {
329+
"type": "plain_text_input",
330+
"action_id": "input_manual_content",
331+
"multiline": True,
332+
"placeholder": {
333+
"type": "plain_text",
334+
"text": "Введіть текст вашої публікації...",
335+
},
336+
},
337+
"label": {
338+
"type": "plain_text",
339+
"text": SLACK_UI["manual_post_modal_content_label"],
340+
"emoji": True,
341+
},
342+
},
343+
{
344+
"type": "input",
345+
"block_id": "block_platform_select",
346+
"element": {
347+
"type": "static_select",
348+
"action_id": "input_platform_select",
349+
"initial_option": {
350+
"text": {"type": "plain_text", "text": "Telegram"},
351+
"value": "telegram",
352+
},
353+
"options": [
354+
{"text": {"type": "plain_text", "text": "Telegram"}, "value": "telegram"},
355+
{"text": {"type": "plain_text", "text": "Twitter"}, "value": "twitter"},
356+
{"text": {"type": "plain_text", "text": "Threads"}, "value": "threads"},
357+
],
358+
},
359+
"label": {
360+
"type": "plain_text",
361+
"text": SLACK_UI["manual_post_modal_platform_label"],
362+
"emoji": True,
363+
},
364+
},
365+
{
366+
"type": "input",
367+
"block_id": "block_schedule_time",
368+
"optional": True,
369+
"element": {
370+
"type": "datetimepicker",
371+
"action_id": "input_schedule_time",
372+
},
373+
"label": {
374+
"type": "plain_text",
375+
"text": SLACK_UI["manual_post_modal_schedule_label"],
376+
"emoji": True,
377+
},
378+
},
379+
],
380+
}
381+
382+
383+
def build_app_home(drafts: list[Draft] | None = None, offset: int = 0, page_size: int = 10) -> dict[str, Any]:
317384
if drafts is None:
318385
drafts = []
319386

@@ -353,6 +420,15 @@ def build_app_home(drafts: list[Draft] | None = None) -> dict[str, Any]:
353420
},
354421
"action_id": "action_open_upload_modal",
355422
},
423+
{
424+
"type": "button",
425+
"text": {
426+
"type": "plain_text",
427+
"text": SLACK_UI["home_btn_manual_post"],
428+
"emoji": True,
429+
},
430+
"action_id": "action_open_manual_post_modal",
431+
},
356432
],
357433
},
358434
{"type": "divider"},
@@ -404,6 +480,28 @@ def build_app_home(drafts: list[Draft] | None = None) -> dict[str, Any]:
404480
}
405481
)
406482

483+
pagination_elements: list[dict[str, Any]] = []
484+
if offset > 0:
485+
pagination_elements.append(
486+
{
487+
"type": "button",
488+
"text": {"type": "plain_text", "text": SLACK_UI["home_btn_prev_page"], "emoji": True},
489+
"value": str(offset - page_size),
490+
"action_id": "action_home_drafts_page",
491+
}
492+
)
493+
if len(drafts) == page_size:
494+
pagination_elements.append(
495+
{
496+
"type": "button",
497+
"text": {"type": "plain_text", "text": SLACK_UI["home_btn_next_page"], "emoji": True},
498+
"value": str(offset + page_size),
499+
"action_id": "action_home_drafts_page",
500+
}
501+
)
502+
if pagination_elements:
503+
blocks.append({"type": "actions", "elements": pagination_elements})
504+
407505
return {"type": "home", "blocks": blocks}
408506

409507

0 commit comments

Comments
 (0)