From b1561458112d670ea2daf9d730e5e412c18dcc6c Mon Sep 17 00:00:00 2001 From: Mish Date: Sun, 21 Dec 2025 11:57:06 +0000 Subject: [PATCH] First iteration of "question tags" (#136) * Rename tags to team tags * Add a TagType enum with TEAM and QUESTION options * Ticket#questionTag exists now * Revert "Add a TagType enum with TEAM and QUESTION options" This reverts commit c9a17f4003aa2ce470f82810ac031fe83325f3d6. * Add a new QuestionTag model * Add another dropdown to the backend msg * create tag => create team tag * Add creating tags (from the backend channel) * Create a single function for sending the backend message Previously this code was duplicated across 2 places, making it inconsistent * Rename a bunch of files; implement upating question tags on tickets * Add a log! * Fix ticket tag dropdowns in old msgs * Auto-select the current question tag when a backend msg is posted * Update backend message with the current tag * Allow clearing the question tag * Dynamically get question tag list * Document question tags * typo * Change some log msgs to debug * Only make the DB call if current_question_tag_id is present * Remove unused variable * Add createdAt to QuestionTag * Ensure "reopened by [user]" is preserved when backend message is edited --- README.md | 12 +- nephthys/actions/assign_question_tag.py | 88 ++++++++++++ .../{assign_tag.py => assign_team_tag.py} | 2 +- nephthys/actions/create_question_tag.py | 49 +++++++ .../{create_tag.py => create_team_tag.py} | 14 +- nephthys/actions/tag_subscribe.py | 2 +- nephthys/events/app_home_opened.py | 6 +- .../events/message/send_backend_message.py | 128 ++++++++++++++++++ .../{message.py => message_creation.py} | 63 +++------ nephthys/macros/reopen.py | 52 +++---- nephthys/options/question_tags.py | 30 ++++ nephthys/options/{tags.py => team_tags.py} | 4 +- nephthys/utils/slack.py | 71 +++++++--- nephthys/views/home/components/header.py | 6 +- nephthys/views/home/{tags.py => team_tags.py} | 50 ++++--- nephthys/views/modals/create_question_tag.py | 43 ++++++ .../{create_tag.py => create_team_tag.py} | 4 +- prisma/schema.prisma | 14 ++ 18 files changed, 498 insertions(+), 140 deletions(-) create mode 100644 nephthys/actions/assign_question_tag.py rename nephthys/actions/{assign_tag.py => assign_team_tag.py} (98%) create mode 100644 nephthys/actions/create_question_tag.py rename nephthys/actions/{create_tag.py => create_team_tag.py} (75%) create mode 100644 nephthys/events/message/send_backend_message.py rename nephthys/events/{message.py => message_creation.py} (88%) create mode 100644 nephthys/options/question_tags.py rename nephthys/options/{tags.py => team_tags.py} (86%) rename nephthys/views/home/{tags.py => team_tags.py} (66%) create mode 100644 nephthys/views/modals/create_question_tag.py rename nephthys/views/modals/{create_tag.py => create_team_tag.py} (91%) diff --git a/README.md b/README.md index 251234c..69f092c 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,17 @@ Nephthys is the bot powering #summer-of-making-help and #identity-help in the Ha ## Features -### Tags +### Question tags -You can tag tickets in the private tickets channel or with the macro `?tag `. This will DM the people who are specialised in responding to those issues and have it show up in their assigned tickets. +Quite often in help channels, the same question gets asked again and again. Helpers can associate tickets with a "question tag", which are pre-defined questions/issue such as "What's the payout range?", "How to top up grant?", or "404 on /shop". We can then keep track of how these questions trend over time, to provide a feedback loop for people building the event/platform/YSWS. + +They can be added to tickets in the private tickets channel. + +### Team tags + +Team tags let you tag tickets that are the responsibility of a specific group of people (or perhaps just one person). E.g. you could have tags for Fufillment, Hack Club Auth, Onboarding flow, etc. + +You can add team tags to tickets in the private tickets channel or with the macro `?tag `. This will DM the people who are specialised in responding to those issues and have it show up in their assigned tickets. You can assign yourself to get notified for specific tags on the app home ### Macros diff --git a/nephthys/actions/assign_question_tag.py b/nephthys/actions/assign_question_tag.py new file mode 100644 index 0000000..bdf1e12 --- /dev/null +++ b/nephthys/actions/assign_question_tag.py @@ -0,0 +1,88 @@ +import logging +from typing import Any +from typing import Dict + +from slack_bolt.async_app import AsyncAck +from slack_sdk.web.async_client import AsyncWebClient + +from nephthys.events.message.send_backend_message import backend_message_blocks +from nephthys.events.message.send_backend_message import backend_message_fallback_text +from nephthys.utils.env import env + + +async def assign_question_tag_callback( + ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient +): + await ack() + user_id = body["user"]["id"] + selected = body["actions"][0]["selected_option"] + selected_value = selected["value"] if selected else None + if selected_value and selected_value.lower() == "none": + return + try: + tag_id = int(selected_value) if selected_value else None + except ValueError as e: + raise ValueError(f"Invalid tag ID: {selected_value}") from e + + channel_id = body["channel"]["id"] + ts = body["message"]["ts"] + + user = await env.db.user.find_unique(where={"slackId": user_id}) + if not user or not user.helper: + logging.warning( + f"Unauthorized user attempted to assign question tag user_id={user_id}" + ) + await client.chat_postEphemeral( + channel=channel_id, + user=user_id, + text="You are not authorized to assign tags.", + ) + return + + ticket = await env.db.ticket.update( + where={"ticketTs": ts}, + data={ + "questionTag": ( + {"connect": {"id": tag_id}} + if tag_id is not None + else {"disconnect": True} + ) + }, + include={"openedBy": True, "reopenedBy": True}, + ) + if not ticket: + logging.error( + f"Failed to find corresponding ticket to update question tag ticket_ts={ts}" + ) + return + + other_tickets = await env.db.ticket.count( + where={ + "openedById": ticket.openedById, + "id": {"not": ticket.id}, + } + ) + if not ticket.openedBy: + logging.error(f"Cannot find who opened ticket ticket_id={ticket.id}") + return + # Update the backend message so it has the new tag selected + await client.chat_update( + channel=channel_id, + ts=ts, + text=backend_message_fallback_text( + author_user_id=ticket.openedBy.slackId, + description=ticket.description, + reopened_by=ticket.reopenedBy, + ), + blocks=await backend_message_blocks( + author_user_id=ticket.openedBy.slackId, + msg_ts=ticket.msgTs, + past_tickets=other_tickets, + current_question_tag_id=tag_id, + reopened_by=ticket.reopenedBy, + ), + ) + + logging.info( + f"Updated question tag on ticket ticket_id={ticket.id} tag_id={tag_id}" + ) diff --git a/nephthys/actions/assign_tag.py b/nephthys/actions/assign_team_tag.py similarity index 98% rename from nephthys/actions/assign_tag.py rename to nephthys/actions/assign_team_tag.py index f717880..1ffe9bb 100644 --- a/nephthys/actions/assign_tag.py +++ b/nephthys/actions/assign_team_tag.py @@ -11,7 +11,7 @@ from nephthys.utils.ticket_methods import get_backend_message_link from nephthys.utils.ticket_methods import get_question_message_link -async def assign_tag_callback( +async def assign_team_tag_callback( ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient ): await ack() diff --git a/nephthys/actions/create_question_tag.py b/nephthys/actions/create_question_tag.py new file mode 100644 index 0000000..ed4b3ae --- /dev/null +++ b/nephthys/actions/create_question_tag.py @@ -0,0 +1,49 @@ +from slack_bolt.async_app import AsyncAck +from slack_sdk.web.async_client import AsyncWebClient + +from nephthys.utils.env import env +from nephthys.utils.logging import send_heartbeat +from nephthys.views.modals.create_question_tag import get_create_question_tag_modal + + +async def create_question_tag_view_callback( + ack: AsyncAck, body: dict, client: AsyncWebClient +): + """ + Callback for the create question tag view submission + """ + await ack() + user_id = body["user"]["id"] + + user = await env.db.user.find_unique(where={"slackId": user_id}) + if not user or not user.helper: + await send_heartbeat( + f"Attempted to create question tag by non-helper <@{user_id}>" + ) + return + + label = body["view"]["state"]["values"]["tag_label"]["tag_label"]["value"] + await env.db.questiontag.create(data={"label": label}) + + # await open_app_home("question-tags", client, user_id) + + +async def create_question_tag_btn_callback( + ack: AsyncAck, body: dict, client: AsyncWebClient +): + """ + Open modal to create a question tag + """ + await ack() + user_id = body["user"]["id"] + trigger_id = body["trigger_id"] + + user = await env.db.user.find_unique(where={"slackId": user_id}) + if not user or not user.helper: + await send_heartbeat( + f"Attempted to open create-question-tag modal by non-helper <@{user_id}>" + ) + return + + view = get_create_question_tag_modal() + await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id) diff --git a/nephthys/actions/create_tag.py b/nephthys/actions/create_team_tag.py similarity index 75% rename from nephthys/actions/create_tag.py rename to nephthys/actions/create_team_tag.py index 0153782..3c6a672 100644 --- a/nephthys/actions/create_tag.py +++ b/nephthys/actions/create_team_tag.py @@ -4,10 +4,12 @@ from slack_sdk.web.async_client import AsyncWebClient from nephthys.events.app_home_opened import open_app_home from nephthys.utils.env import env from nephthys.utils.logging import send_heartbeat -from nephthys.views.modals.create_tag import get_create_tag_modal +from nephthys.views.modals.create_team_tag import get_create_team_tag_modal -async def create_tag_view_callback(ack: AsyncAck, body: dict, client: AsyncWebClient): +async def create_team_tag_view_callback( + ack: AsyncAck, body: dict, client: AsyncWebClient +): """ Callback for the create tag view submission """ @@ -22,10 +24,12 @@ async def create_tag_view_callback(ack: AsyncAck, body: dict, client: AsyncWebCl name = body["view"]["state"]["values"]["tag_name"]["tag_name"]["value"] await env.db.tag.create(data={"name": name}) - await open_app_home("tags", client, user_id) + await open_app_home("team-tags", client, user_id) -async def create_tag_btn_callback(ack: AsyncAck, body: dict, client: AsyncWebClient): +async def create_team_tag_btn_callback( + ack: AsyncAck, body: dict, client: AsyncWebClient +): """ Open modal to create a tag """ @@ -40,5 +44,5 @@ async def create_tag_btn_callback(ack: AsyncAck, body: dict, client: AsyncWebCli ) return - view = get_create_tag_modal() + view = get_create_team_tag_modal() await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id) diff --git a/nephthys/actions/tag_subscribe.py b/nephthys/actions/tag_subscribe.py index 0a8f9aa..18e2397 100644 --- a/nephthys/actions/tag_subscribe.py +++ b/nephthys/actions/tag_subscribe.py @@ -41,4 +41,4 @@ async def tag_subscribe_callback( } ) - await open_app_home("tags", client, slack_id) + await open_app_home("team-tags", client, slack_id) diff --git a/nephthys/events/app_home_opened.py b/nephthys/events/app_home_opened.py index 128bff8..df807aa 100644 --- a/nephthys/events/app_home_opened.py +++ b/nephthys/events/app_home_opened.py @@ -14,7 +14,7 @@ from nephthys.views.home.error import get_error_view from nephthys.views.home.helper import get_helper_view from nephthys.views.home.loading import get_loading_view from nephthys.views.home.stats import get_stats_view -from nephthys.views.home.tags import get_manage_tags_view +from nephthys.views.home.team_tags import get_team_tags_view DEFAULT_VIEW = "dashboard" @@ -54,8 +54,8 @@ async def open_app_home(home_type: str, client: AsyncWebClient, user_id: str): view = await get_helper_view(slack_user=user_id, db_user=user) case "assigned-tickets": view = await get_assigned_tickets_view(user) - case "tags": - view = await get_manage_tags_view(user) + case "team-tags": + view = await get_team_tags_view(user) case "my-stats": view = await get_stats_view(user) case _: diff --git a/nephthys/events/message/send_backend_message.py b/nephthys/events/message/send_backend_message.py new file mode 100644 index 0000000..32f3a38 --- /dev/null +++ b/nephthys/events/message/send_backend_message.py @@ -0,0 +1,128 @@ +from slack_sdk.web.async_client import AsyncWebClient + +from nephthys.utils.env import env +from prisma.models import User + + +async def backend_message_blocks( + author_user_id: str, + msg_ts: str, + past_tickets: int, + current_question_tag_id: int | None = None, + reopened_by: User | None = None, +) -> list[dict]: + thread_url = f"https://hackclub.slack.com/archives/{env.slack_help_channel}/p{msg_ts.replace('.', '')}" + if current_question_tag_id is not None: + options = [ + { + "text": { + "type": "plain_text", + "text": tag.label, + }, + "value": f"{tag.id}", + } + for tag in await env.db.questiontag.find_many() + ] + initial_option = [ + option + for option in options + if option["value"] == f"{current_question_tag_id}" + ][0] + else: + initial_option = None + question_tags_dropdown = { + "type": "input", + "label": {"type": "plain_text", "text": "Question tag", "emoji": True}, + "element": { + "type": "external_select", + "action_id": "question-tag-list", + "placeholder": { + "type": "plain_text", + "text": "Which question tag fits?", + }, + "min_query_length": 0, + }, + } + if initial_option: + question_tags_dropdown["element"]["initial_option"] = initial_option + + return [ + question_tags_dropdown, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "If none of the existing tags fit :point_right:", + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": ":wrench: new question tag", + "emoji": True, + }, + "action_id": "create-question-tag", + }, + }, + { + "type": "input", + "label": {"type": "plain_text", "text": "Team tags", "emoji": True}, + "element": { + "action_id": "team-tag-list", + "type": "multi_external_select", + "placeholder": {"type": "plain_text", "text": "Select tags"}, + "min_query_length": 0, + }, + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": ( + f"Reopened by <@{reopened_by.slackId}>. Originally submitted by <@{author_user_id}>. <{thread_url}|View thread>." + if reopened_by + else f"Submitted by <@{author_user_id}>. They have {past_tickets} past tickets. <{thread_url}|View thread>." + ), + } + ], + }, + ] + + +def backend_message_fallback_text( + author_user_id: str, + description: str, + reopened_by: User | None = None, +) -> str: + return ( + f"Reopened ticket from <@{author_user_id}>: {description}" + if reopened_by + else f"New question from <@{author_user_id}>: {description}" + ) + + +async def send_backend_message( + author_user_id: str, + msg_ts: str, + description: str, + past_tickets: int, + client: AsyncWebClient, + current_question_tag_id: int | None = None, + reopened_by: User | None = None, + display_name: str | None = None, + profile_pic: str | None = None, +): + """Send a "backend" message to the tickets channel with ticket details.""" + + return await client.chat_postMessage( + channel=env.slack_ticket_channel, + text=backend_message_fallback_text(author_user_id, description, reopened_by), + blocks=await backend_message_blocks( + author_user_id, msg_ts, past_tickets, current_question_tag_id, reopened_by + ), + username=display_name, + icon_url=profile_pic, + unfurl_links=True, + unfurl_media=True, + ) diff --git a/nephthys/events/message.py b/nephthys/events/message_creation.py similarity index 88% rename from nephthys/events/message.py rename to nephthys/events/message_creation.py index 317fb4c..edffa74 100644 --- a/nephthys/events/message.py +++ b/nephthys/events/message_creation.py @@ -8,6 +8,7 @@ from prometheus_client import Histogram from slack_sdk.errors import SlackApiError from slack_sdk.web.async_client import AsyncWebClient +from nephthys.events.message.send_backend_message import send_backend_message from nephthys.macros import run_macro from nephthys.utils.env import env from nephthys.utils.logging import send_heartbeat @@ -104,49 +105,6 @@ async def handle_message_in_thread(event: Dict[str, Any], db_user: User | None): ) -async def send_ticket_message( - event: Dict[str, Any], - client: AsyncWebClient, - past_tickets: int, - display_name: str | None, - profile_pic: str | None, -): - """Send a "backend" message to the tickets channel with ticket details.""" - user = event.get("user", "unknown") - text = event.get("text", "") - thread_url = f"https://hackclub.slack.com/archives/{env.slack_help_channel}/p{event['ts'].replace('.', '')}" - - return await client.chat_postMessage( - channel=env.slack_ticket_channel, - text=f"New message from <@{user}>: {text}", - blocks=[ - { - "type": "input", - "label": {"type": "plain_text", "text": "Tag ticket", "emoji": True}, - "element": { - "action_id": "tag-list", - "type": "multi_external_select", - "placeholder": {"type": "plain_text", "text": "Select tags"}, - "min_query_length": 0, - }, - }, - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": f"Submitted by <@{user}>. They have {past_tickets} past tickets. <{thread_url}|View thread>.", - } - ], - }, - ], - username=display_name, - icon_url=profile_pic, - unfurl_links=True, - unfurl_media=True, - ) - - async def handle_new_question( event: Dict[str, Any], client: AsyncWebClient, db_user: User | None ): @@ -183,10 +141,12 @@ async def handle_new_question( ) async with perf_timer("Sending backend ticket message"): - ticket_message = await send_ticket_message( - event, - client, + ticket_message = await send_backend_message( + author_user_id=author_id, + description=text, + msg_ts=event["ts"], past_tickets=past_tickets, + client=client, display_name=author.display_name(), profile_pic=author.profile_pic_512x() or "", ) @@ -282,7 +242,16 @@ async def send_user_facing_message( "style": "primary", "action_id": "mark_resolved", "value": f"{event['ts']}", - } + }, + # { + # "type": "button", + # "text": { + # "type": "plain_text", + # "text": "Manage tags (helpers only)", + # }, + # "action_id": "manage_tags_from_thread", + # "value": f"{event['ts']}", + # }, ], }, { diff --git a/nephthys/macros/reopen.py b/nephthys/macros/reopen.py index 2829ab1..efcedc7 100644 --- a/nephthys/macros/reopen.py +++ b/nephthys/macros/reopen.py @@ -2,11 +2,11 @@ import logging from slack_sdk.errors import SlackApiError +from nephthys.events.message.send_backend_message import send_backend_message from nephthys.macros.types import Macro from nephthys.utils.env import env from nephthys.utils.logging import send_heartbeat from nephthys.utils.slack_user import get_user_profile -from nephthys.utils.ticket_methods import get_question_message_link from nephthys.utils.ticket_methods import reply_to_ticket from prisma.enums import TicketStatus @@ -28,6 +28,7 @@ class Reopen(Macro): data={ "status": TicketStatus.OPEN, "closedBy": {"disconnect": True}, + "reopenedBy": {"connect": {"id": helper.id}}, "closedAt": None, }, ) @@ -45,40 +46,23 @@ class Reopen(Macro): return author_id = ticket.openedBy.slackId author = await get_user_profile(author_id) - thread_url = get_question_message_link(ticket) + other_tickets = await env.db.ticket.count( + where={ + "openedById": ticket.openedById, + "id": {"not": ticket.id}, + } + ) - backend_message = await env.slack_client.chat_postMessage( - channel=env.slack_ticket_channel, - text=f"Reopened ticket from <@{author_id}>: {ticket.description}", - blocks=[ - { - "type": "input", - "label": { - "type": "plain_text", - "text": "Tag ticket", - "emoji": True, - }, - "element": { - "action_id": "tag-list", - "type": "multi_external_select", - "placeholder": {"type": "plain_text", "text": "Select tags"}, - "min_query_length": 0, - }, - }, - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": f"Reopened by <@{helper.slackId}>. Originally submitted by <@{author_id}>. <{thread_url}|View thread>.", - } - ], - }, - ], - username=author.display_name(), - icon_url=author.profile_pic_512x() or "", - unfurl_links=True, - unfurl_media=True, + backend_message = await send_backend_message( + author_user_id=author_id, + description=ticket.description, + msg_ts=ticket.msgTs, + past_tickets=other_tickets, + client=env.slack_client, + current_question_tag_id=ticket.questionTagId, + reopened_by=helper, + display_name=author.display_name(), + profile_pic=author.profile_pic_512x(), ) new_ticket_ts = backend_message["ts"] diff --git a/nephthys/options/question_tags.py b/nephthys/options/question_tags.py new file mode 100644 index 0000000..102a86d --- /dev/null +++ b/nephthys/options/question_tags.py @@ -0,0 +1,30 @@ +import logging + +from thefuzz import fuzz +from thefuzz import process + +from nephthys.utils.env import env + + +async def get_question_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]: + tags = await env.db.questiontag.find_many() + if not tags: + return [] + + keyword = payload.get("value") + if keyword: + tag_names = [tag.label for tag in tags] + scores = process.extract(keyword, tag_names, scorer=fuzz.ratio, limit=100) + matching_tags = [tags[tag_names.index(score[0])] for score in scores] + else: + matching_tags = tags + + res = [ + { + "text": {"type": "plain_text", "text": f"{tag.label}"}, + "value": str(tag.id), + } + for tag in matching_tags + ] + logging.debug(res) + return res diff --git a/nephthys/options/tags.py b/nephthys/options/team_tags.py similarity index 86% rename from nephthys/options/tags.py rename to nephthys/options/team_tags.py index 31a132a..fef6c82 100644 --- a/nephthys/options/tags.py +++ b/nephthys/options/team_tags.py @@ -6,7 +6,7 @@ from thefuzz import process from nephthys.utils.env import env -async def get_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]: +async def get_team_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]: tags = await env.db.tag.find_many() if not tags: return [] @@ -25,5 +25,5 @@ async def get_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]: } for tag in tags ] - logging.info(res) + logging.debug(res) return res diff --git a/nephthys/utils/slack.py b/nephthys/utils/slack.py index dd7fa57..cd38db5 100644 --- a/nephthys/utils/slack.py +++ b/nephthys/utils/slack.py @@ -6,9 +6,12 @@ from slack_bolt.async_app import AsyncApp from slack_bolt.context.ack.async_ack import AsyncAck from slack_sdk.web.async_client import AsyncWebClient -from nephthys.actions.assign_tag import assign_tag_callback -from nephthys.actions.create_tag import create_tag_btn_callback -from nephthys.actions.create_tag import create_tag_view_callback +from nephthys.actions.assign_question_tag import assign_question_tag_callback +from nephthys.actions.assign_team_tag import assign_team_tag_callback +from nephthys.actions.create_question_tag import create_question_tag_btn_callback +from nephthys.actions.create_question_tag import create_question_tag_view_callback +from nephthys.actions.create_team_tag import create_team_tag_btn_callback +from nephthys.actions.create_team_tag import create_team_tag_view_callback from nephthys.actions.resolve import resolve from nephthys.actions.tag_subscribe import tag_subscribe_callback from nephthys.commands.dm_magic_link import dm_magic_link_cmd_callback @@ -16,9 +19,10 @@ from nephthys.events.app_home_opened import on_app_home_opened from nephthys.events.app_home_opened import open_app_home from nephthys.events.channel_join import channel_join from nephthys.events.channel_left import channel_left -from nephthys.events.message import on_message +from nephthys.events.message_creation import on_message from nephthys.events.message_deletion import on_message_deletion -from nephthys.options.tags import get_tags +from nephthys.options.question_tags import get_question_tags +from nephthys.options.team_tags import get_team_tags from nephthys.utils.env import env from nephthys.utils.performance import perf_timer @@ -51,9 +55,16 @@ async def handle_mark_resolved_button( await resolve(value, resolver, client) -@app.options("tag-list") -async def handle_tag_list_options(ack: AsyncAck, payload: dict): - tags = await get_tags(payload) +@app.options("tag-list") # compat with old backend msgs +@app.options("team-tag-list") +async def handle_team_tag_list_options(ack: AsyncAck, payload: dict): + tags = await get_team_tags(payload) + await ack(options=tags) + + +@app.options("question-tag-list") +async def handle_question_tag_list_options(ack: AsyncAck, payload: dict): + tags = await get_question_tags(payload) await ack(options=tags) @@ -64,7 +75,7 @@ async def app_home_opened_handler(event: dict[str, Any], client: AsyncWebClient) @app.action("dashboard") @app.action("assigned-tickets") -@app.action("tags") +@app.action("team-tags") @app.action("my-stats") async def manage_home_switcher(ack: AsyncAck, body, client: AsyncWebClient): await ack() @@ -84,14 +95,30 @@ async def handle_member_left_channel(event: Dict[str, Any], client: AsyncWebClie await channel_left(ack=AsyncAck(), event=event, client=client) -@app.action("create-tag") -async def create_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient): - await create_tag_btn_callback(ack, body, client) +@app.action("create-team-tag") +async def create_team_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient): + await create_team_tag_btn_callback(ack, body, client) -@app.view("create_tag") -async def create_tag_view(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient): - await create_tag_view_callback(ack, body, client) +@app.action("create-question-tag") +async def create_question_tag( + ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient +): + await create_question_tag_btn_callback(ack, body, client) + + +@app.view("create_team_tag") +async def create_team_tag_view( + ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient +): + await create_team_tag_view_callback(ack, body, client) + + +@app.view("create_question_tag") +async def create_question_tag_view( + ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient +): + await create_question_tag_view_callback(ack, body, client) @app.action("tag-subscribe") @@ -99,9 +126,17 @@ async def tag_subscribe(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebCli await tag_subscribe_callback(ack, body, client) -@app.action("tag-list") -async def assign_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient): - await assign_tag_callback(ack, body, client) +@app.action("tag-list") # compat with old backend msgs +@app.action("team-tag-list") +async def assign_team_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient): + await assign_team_tag_callback(ack, body, client) + + +@app.action("question-tag-list") +async def assign_question_tag( + ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient +): + await assign_question_tag_callback(ack, body, client) @app.command("/dm-magic-link") diff --git a/nephthys/views/home/components/header.py b/nephthys/views/home/components/header.py index 795f7a2..c4cfd7f 100644 --- a/nephthys/views/home/components/header.py +++ b/nephthys/views/home/components/header.py @@ -26,9 +26,9 @@ def header_buttons(current_view: str): buttons.append( { "type": "button", - "text": {"type": "plain_text", "text": "Tags", "emoji": True}, - "action_id": "tags", - **({"style": "primary"} if current_view != "tags" else {}), + "text": {"type": "plain_text", "text": "Team Tags", "emoji": True}, + "action_id": "team-tags", + **({"style": "primary"} if current_view != "team-tags" else {}), } ) diff --git a/nephthys/views/home/tags.py b/nephthys/views/home/team_tags.py similarity index 66% rename from nephthys/views/home/tags.py rename to nephthys/views/home/team_tags.py index dac854d..91d34c1 100644 --- a/nephthys/views/home/tags.py +++ b/nephthys/views/home/team_tags.py @@ -5,8 +5,8 @@ from nephthys.views.home.components.header import get_header from prisma.models import User -async def get_manage_tags_view(user: User | None) -> dict: - header = get_header(user, "tags") +async def get_team_tags_view(user: User | None) -> dict: + header = get_header(user, "team-tags") is_admin = bool(user and user.admin) is_helper = bool(user and user.helper) tags = await env.db.tag.find_many(include={"userSubscriptions": True}) @@ -44,19 +44,21 @@ async def get_manage_tags_view(user: User | None) -> dict: "type": "mrkdwn", "text": f"*{tag.name}* - {''.join(stringified_subs) if stringified_subs else ':rac_nooo: no subscriptions'}", }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": f":rac_cute: {'subscribe' if user.id not in subs else 'unsubscribe'}", - "emoji": True, - }, - "action_id": "tag-subscribe", - "value": f"{tag.id};{tag.name}", - "style": "primary" if user.id not in subs else "danger", - } - if user and is_helper - else {}, + "accessory": ( + { + "type": "button", + "text": { + "type": "plain_text", + "text": f":rac_cute: {'subscribe' if user.id not in subs else 'unsubscribe'}", + "emoji": True, + }, + "action_id": "tag-subscribe", + "value": f"{tag.id};{tag.name}", + "style": "primary" if user.id not in subs else "danger", + } + if user and is_helper + else {} + ), } ) @@ -68,7 +70,7 @@ async def get_manage_tags_view(user: User | None) -> dict: "type": "header", "text": { "type": "plain_text", - "text": ":rac_info: Manage Tags", + "text": ":rac_info: Manage Team Tags", "emoji": True, }, }, @@ -76,11 +78,15 @@ async def get_manage_tags_view(user: User | None) -> dict: "type": "section", "text": { "type": "mrkdwn", - "text": ":rac_thumbs: here you can manage tags and your subscriptions" - if is_admin - else ":rac_thumbs: here you can manage your tag subscriptions" - if is_helper - else ":rac_thumbs: note: you're not a helper, so you can only view tags", + "text": ( + ":rac_thumbs: here you can manage tags and your subscriptions" + if is_admin + else ( + ":rac_thumbs: here you can manage your tag subscriptions" + if is_helper + else ":rac_thumbs: note: you're not a helper, so you can only view tags" + ) + ), }, }, {"type": "section", "text": {"type": "plain_text", "text": " "}}, @@ -100,7 +106,7 @@ async def get_manage_tags_view(user: User | None) -> dict: "text": ":rac_cute: add a tag?", "emoji": True, }, - "action_id": "create-tag", + "action_id": "create-team-tag", "style": "primary", } ], diff --git a/nephthys/views/modals/create_question_tag.py b/nephthys/views/modals/create_question_tag.py new file mode 100644 index 0000000..d645435 --- /dev/null +++ b/nephthys/views/modals/create_question_tag.py @@ -0,0 +1,43 @@ +def get_create_question_tag_modal(): + return { + "type": "modal", + "callback_id": "create_question_tag", + "title": { + "type": "plain_text", + "text": "Create question tag", + "emoji": True, + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": """ +Question tags are used to keep track of how often specific questions are being asked, so tag names should be brief but specific enough to uniquely identify the question. + +Examples: +• Missing CSS on site +• What is Flavortown? +• "You're not eligible" when trying to ship project + """, + }, + }, + { + "type": "input", + "block_id": "tag_label", + "label": { + "type": "plain_text", + "text": "Question label", + }, + "element": { + "type": "plain_text_input", + "action_id": "tag_label", + }, + }, + ], + "submit": { + "type": "plain_text", + "text": ":rac_question: Create!", + "emoji": True, + }, + } diff --git a/nephthys/views/modals/create_tag.py b/nephthys/views/modals/create_team_tag.py similarity index 91% rename from nephthys/views/modals/create_tag.py rename to nephthys/views/modals/create_team_tag.py index a28ef8c..9519665 100644 --- a/nephthys/views/modals/create_tag.py +++ b/nephthys/views/modals/create_team_tag.py @@ -1,7 +1,7 @@ -def get_create_tag_modal(): +def get_create_team_tag_modal(): return { "type": "modal", - "callback_id": "create_tag", + "callback_id": "create_team_tag", "title": { "type": "plain_text", "text": ":rac_info: create a tag!", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a4d240d..0a78bc6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,6 +30,7 @@ model User { openedTickets Ticket[] @relation("OpenedTickets") closedTickets Ticket[] @relation("ClosedTickets") assignedTickets Ticket[] @relation("AssignedTickets") + reopenedTickets Ticket[] @relation("ReopenedTickets") tagSubscriptions UserTagSubscription[] helper Boolean @default(false) @@ -57,6 +58,9 @@ model Ticket { openedBy User @relation("OpenedTickets", fields: [openedById], references: [id]) openedById Int + reopenedBy User? @relation("ReopenedTickets", fields: [reopenedById], references: [id]) + reopenedById Int? + closedBy User? @relation("ClosedTickets", fields: [closedById], references: [id]) closedById Int? closedAt DateTime? @@ -66,10 +70,20 @@ model Ticket { assignedAt DateTime? tagsOnTickets TagsOnTickets[] + questionTag QuestionTag? @relation("QuestionTagTickets", fields: [questionTagId], references: [id]) + questionTagId Int? createdAt DateTime @default(now()) } +model QuestionTag { + id Int @id @unique @default(autoincrement()) + label String @unique + tickets Ticket[] @relation("QuestionTagTickets") + createdAt DateTime @default(now()) +} + +// These tags are team tags model Tag { id Int @id @unique @default(autoincrement()) name String @unique