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
This commit is contained in:
Mish 2025-12-21 11:57:06 +00:00 committed by GitHub
parent 983f895737
commit b156145811
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 498 additions and 140 deletions

View file

@ -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 <tag_name>`. 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 <tag_name>`. 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

View file

@ -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}"
)

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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 _:

View file

@ -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,
)

View file

@ -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']}",
# },
],
},
{

View file

@ -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"]

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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 {}),
}
)

View file

@ -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",
}
],

View file

@ -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,
},
}

View file

@ -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!",

View file

@ -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