editing and slugs and descs, i think i did slugs right?

This commit is contained in:
End 2026-03-11 23:51:47 -07:00
parent 575fa89192
commit 2e77f9f225
No known key found for this signature in database
12 changed files with 414 additions and 38 deletions

View file

@ -1,4 +1,5 @@
import logging
import re
from slack_bolt.async_app import AsyncAck
from slack_sdk.web.async_client import AsyncWebClient
@ -31,17 +32,31 @@ async def create_category_tag_view_callback(
ack: AsyncAck, body: dict, client: AsyncWebClient
):
user_id = body["user"]["id"]
values = body["view"]["state"]["values"]
raw_name = body["view"]["state"]["values"]["category_tag_name"][
"category_tag_name"
]["value"]
raw_name = values["category_tag_name"]["category_tag_name"]["value"]
name = raw_name.strip() if raw_name else ""
raw_slug = values["category_tag_slug"]["category_tag_slug"]["value"]
slug = raw_slug.strip() if raw_slug else None
raw_description = values["category_tag_description"]["category_tag_description"][
"value"
]
description = raw_description.strip() if raw_description else None
errors = {}
if not name:
await ack(
response_action="errors",
errors={"category_tag_name": "Category name cannot be empty."},
errors["category_tag_name"] = "Category name cannot be empty."
if slug and not re.match(r"^[a-z0-9_]+$", slug):
errors["category_tag_slug"] = (
"Slug must be snake_case (lowercase letters, numbers, and underscores only)."
)
if errors:
await ack(response_action="errors", errors=errors)
return
user = await env.db.user.find_unique(where={"slackId": user_id})
@ -53,17 +68,20 @@ async def create_category_tag_view_callback(
return
try:
await env.db.categorytag.create(
data={"name": name, "createdBy": {"connect": {"id": user.id}}}
)
except UniqueViolationError:
logging.warning(f"Duplicate category tag name: {name}")
await ack(
response_action="errors",
errors={
"category_tag_name": f"A category tag named '{name}' already exists."
},
data: dict = {"name": name, "createdBy": {"connect": {"id": user.id}}}
if slug:
data["slug"] = slug
if description:
data["description"] = description
await env.db.categorytag.create(data=data)
except UniqueViolationError as e:
logging.warning(f"Duplicate category tag: {e}")
error_field = (
"category_tag_slug" if slug and "slug" in str(e) else "category_tag_name"
)
error_msg = f"A category tag with this {'slug' if error_field == 'category_tag_slug' else 'name'} already exists."
await ack(response_action="errors", errors={error_field: error_msg})
return
await ack()

View file

@ -21,9 +21,17 @@ async def create_team_tag_view_callback(
await send_heartbeat(f"Attempted to create tag by non-admin user <@{user_id}>")
return
name = body["view"]["state"]["values"]["tag_name"]["tag_name"]["value"]
await env.db.tag.create(data={"name": name})
values = body["view"]["state"]["values"]
name = values["tag_name"]["tag_name"]["value"]
raw_description = values["tag_description"]["tag_description"]["value"]
description = raw_description.strip() if raw_description else None
data: dict = {"name": name}
if description:
data["description"] = description
await env.db.tag.create(data=data)
await open_app_home("team-tags", client, user_id)

View file

@ -0,0 +1,92 @@
import logging
import re
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.edit_category_tag import get_edit_category_tag_modal
from prisma.errors import UniqueViolationError
async def edit_category_tag_btn_callback(
ack: AsyncAck, body: dict, client: AsyncWebClient
):
await ack()
user_id = body["user"]["id"]
trigger_id = body["trigger_id"]
tag_id = int(body["actions"][0]["value"])
user = await env.db.user.find_unique(where={"slackId": user_id})
if not user or not user.admin:
await send_heartbeat(
f"Attempted to open edit category tag modal by non-admin user <@{user_id}>"
)
return
tag = await env.db.categorytag.find_unique(where={"id": tag_id})
if not tag:
logging.error(f"Category tag not found: id={tag_id}")
return
view = get_edit_category_tag_modal(tag.id, tag.name, tag.slug, tag.description)
await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id)
async def edit_category_tag_view_callback(
ack: AsyncAck, body: dict, client: AsyncWebClient
):
user_id = body["user"]["id"]
callback_id = body["view"]["callback_id"]
tag_id = int(callback_id.replace("edit_category_tag_", ""))
values = body["view"]["state"]["values"]
raw_name = values["category_tag_name"]["category_tag_name"]["value"]
name = raw_name.strip() if raw_name else ""
raw_slug = values["category_tag_slug"]["category_tag_slug"]["value"]
slug = raw_slug.strip() if raw_slug else None
raw_description = values["category_tag_description"]["category_tag_description"]["value"]
description = raw_description.strip() if raw_description else None
errors = {}
if not name:
errors["category_tag_name"] = "Category name cannot be empty."
if slug and not re.match(r"^[a-z0-9_]+$", slug):
errors["category_tag_slug"] = (
"Slug must be snake_case (lowercase letters, numbers, and underscores only)."
)
if errors:
await ack(response_action="errors", errors=errors)
return
user = await env.db.user.find_unique(where={"slackId": user_id})
if not user or not user.admin:
await ack()
await send_heartbeat(
f"Attempted to edit category tag by non-admin user <@{user_id}>"
)
return
try:
await env.db.categorytag.update(
where={"id": tag_id},
data={"name": name, "slug": slug, "description": description},
)
except UniqueViolationError as e:
logging.warning(f"Duplicate category tag: {e}")
error_field = "category_tag_slug" if slug and "slug" in str(e) else "category_tag_name"
error_msg = f"A category tag with this {'slug' if error_field == 'category_tag_slug' else 'name'} already exists."
await ack(response_action="errors", errors={error_field: error_msg})
return
await ack()
from nephthys.events.app_home_opened import open_app_home
await open_app_home("category-tags", client, user_id)

View file

@ -0,0 +1,65 @@
import logging
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.edit_team_tag import get_edit_team_tag_modal
async def edit_team_tag_btn_callback(
ack: AsyncAck, body: dict, client: AsyncWebClient
):
await ack()
user_id = body["user"]["id"]
trigger_id = body["trigger_id"]
tag_id = int(body["actions"][0]["value"])
user = await env.db.user.find_unique(where={"slackId": user_id})
if not user or not user.admin:
await send_heartbeat(
f"Attempted to open edit team tag modal by non-admin user <@{user_id}>"
)
return
tag = await env.db.tag.find_unique(where={"id": tag_id})
if not tag:
logging.error(f"Team tag not found: id={tag_id}")
return
view = get_edit_team_tag_modal(tag.id, tag.name, tag.description)
await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id)
async def edit_team_tag_view_callback(
ack: AsyncAck, body: dict, client: AsyncWebClient
):
await ack()
user_id = body["user"]["id"]
callback_id = body["view"]["callback_id"]
tag_id = int(callback_id.replace("edit_team_tag_", ""))
values = body["view"]["state"]["values"]
name = values["tag_name"]["tag_name"]["value"]
raw_description = values["tag_description"]["tag_description"]["value"]
description = raw_description.strip() if raw_description else None
user = await env.db.user.find_unique(where={"slackId": user_id})
if not user or not user.admin:
await send_heartbeat(
f"Attempted to edit team tag by non-admin user <@{user_id}>"
)
return
await env.db.tag.update(
where={"id": tag_id},
data={"name": name, "description": description},
)
logging.info(f"Updated team tag id={tag_id} name={name} by <@{user_id}>")
from nephthys.events.app_home_opened import open_app_home
await open_app_home("team-tags", client, user_id)

View file

@ -1,4 +1,5 @@
import logging
import re
from typing import Any
from typing import Dict
@ -12,6 +13,10 @@ from nephthys.actions.create_category_tag import create_category_tag_btn_callbac
from nephthys.actions.create_category_tag import create_category_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.edit_category_tag import edit_category_tag_btn_callback
from nephthys.actions.edit_category_tag import edit_category_tag_view_callback
from nephthys.actions.edit_team_tag import edit_team_tag_btn_callback
from nephthys.actions.edit_team_tag import edit_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
@ -108,6 +113,18 @@ async def create_team_tag_view(
await create_team_tag_view_callback(ack, body, client)
@app.action(re.compile(r"^edit-team-tag-\d+$"))
async def edit_team_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
await edit_team_tag_btn_callback(ack, body, client)
@app.view(re.compile(r"^edit_team_tag_\d+$"))
async def edit_team_tag_view(
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
):
await edit_team_tag_view_callback(ack, body, client)
@app.action("create-category-tag")
async def create_category_tag(
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
@ -122,6 +139,20 @@ async def create_category_tag_view(
await create_category_tag_view_callback(ack, body, client)
@app.action(re.compile(r"^edit-category-tag-\d+$"))
async def edit_category_tag(
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
):
await edit_category_tag_btn_callback(ack, body, client)
@app.view(re.compile(r"^edit_category_tag_\d+$"))
async def edit_category_tag_view(
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
):
await edit_category_tag_view_callback(ack, body, client)
@app.action("tag-subscribe")
async def tag_subscribe(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
await tag_subscribe_callback(ack, body, client)

View file

@ -31,7 +31,22 @@ async def get_category_tags_view(user: User | None) -> dict:
tag_blocks.append(Section(":rac_nooo: no category tags yet — add one below!"))
else:
for tag in category_tags:
tag_blocks.append(Section(f"*{tag.name}*"))
text = f"*{tag.name}*"
if tag.slug:
text += f"\n`{tag.slug}`"
if tag.description:
text += f"\n_{tag.description}_"
tag_blocks.append(
Section(
text=text,
accessory=Button(
text=":pencil2: Edit",
action_id=f"edit-category-tag-{tag.id}",
value=str(tag.id),
),
)
)
return Home(
[

View file

@ -37,12 +37,18 @@ async def get_team_tags_view(user: User | None) -> dict:
else:
subs = []
stringified_subs = [f"<@{user}>" for user in subs]
tag_text = f"*{tag.name}*"
if tag.description:
tag_text += f"\n_{tag.description}_"
tag_text += f"\n{''.join(stringified_subs) if stringified_subs else ':rac_nooo: no subscriptions'}"
blocks.append(
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{tag.name}* - {''.join(stringified_subs) if stringified_subs else ':rac_nooo: no subscriptions'}",
"text": tag_text,
},
"accessory": (
{
@ -62,6 +68,25 @@ async def get_team_tags_view(user: User | None) -> dict:
}
)
if is_admin:
blocks.append(
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": ":pencil2: Edit tag",
"emoji": True,
},
"action_id": f"edit-team-tag-{tag.id}",
"value": str(tag.id),
}
],
}
)
view = {
"type": "home",
"blocks": [

View file

@ -14,7 +14,24 @@ def get_create_category_tag_modal():
block_id="category_tag_name",
element=PlainTextInput(action_id="category_tag_name"),
),
# future: description
# future: slug
Input(
label="Slug (snake_case ID, optional)",
block_id="category_tag_slug",
optional=True,
element=PlainTextInput(
action_id="category_tag_slug",
placeholder="e.g. payouts_issue or fulfillment_query",
),
),
Input(
label="Description (helps AI pick this category)",
block_id="category_tag_description",
optional=True,
element=PlainTextInput(
action_id="category_tag_description",
multiline=True,
placeholder="What kinds of questions should be tagged with this?",
),
),
],
).build()

View file

@ -7,13 +7,18 @@ def get_create_team_tag_modal():
"text": ":rac_info: create a tag!",
"emoji": True,
},
"submit": {
"type": "plain_text",
"text": ":rac_question: add tag?",
"emoji": True,
},
"blocks": [
{
"type": "input",
"block_id": "tag_name",
"label": {
"type": "plain_text",
"text": "giv name?",
"text": "Name",
"emoji": True,
},
"element": {
@ -21,10 +26,24 @@ def get_create_team_tag_modal():
"action_id": "tag_name",
},
},
{
"type": "input",
"block_id": "tag_description",
"optional": True,
"label": {
"type": "plain_text",
"text": "Description (optional)",
"emoji": True,
},
"element": {
"type": "plain_text_input",
"action_id": "tag_description",
"multiline": True,
"placeholder": {
"type": "plain_text",
"text": "What is this tag for?",
},
},
},
],
"submit": {
"type": "plain_text",
"text": ":rac_question: add tag?",
"emoji": True,
},
}

View file

@ -0,0 +1,47 @@
from blockkit import Input
from blockkit import Modal
from blockkit import PlainTextInput
def get_edit_category_tag_modal(
tag_id: int,
name: str,
slug: str | None,
description: str | None,
):
return Modal(
title=":pencil2: edit category",
callback_id=f"edit_category_tag_{tag_id}",
submit=":rac_thumbs: save",
blocks=[
Input(
label="Category name",
block_id="category_tag_name",
element=PlainTextInput(
action_id="category_tag_name",
initial_value=name,
),
),
Input(
label="Slug (snake_case ID, optional)",
block_id="category_tag_slug",
optional=True,
element=PlainTextInput(
action_id="category_tag_slug",
placeholder="e.g. payouts_issue or fulfillment_query",
**({"initial_value": slug} if slug else {}),
),
),
Input(
label="Description (helps AI pick this category)",
block_id="category_tag_description",
optional=True,
element=PlainTextInput(
action_id="category_tag_description",
multiline=True,
placeholder="What kinds of questions should be tagged with this?",
**({"initial_value": description} if description else {}),
),
),
],
).build()

View file

@ -0,0 +1,36 @@
def get_edit_team_tag_modal(tag_id: int, name: str, description: str | None):
return {
"type": "modal",
"callback_id": f"edit_team_tag_{tag_id}",
"title": {"type": "plain_text", "text": ":pencil2: edit tag", "emoji": True},
"submit": {"type": "plain_text", "text": ":rac_thumbs: save", "emoji": True},
"blocks": [
{
"type": "input",
"block_id": "tag_name",
"label": {"type": "plain_text", "text": "Name", "emoji": True},
"element": {
"type": "plain_text_input",
"action_id": "tag_name",
"initial_value": name,
},
},
{
"type": "input",
"block_id": "tag_description",
"optional": True,
"label": {
"type": "plain_text",
"text": "Description (optional)",
"emoji": True,
},
"element": {
"type": "plain_text_input",
"action_id": "tag_description",
"multiline": True,
"placeholder": {"type": "plain_text", "text": "What is this tag for?"},
**({"initial_value": description} if description else {}),
},
},
],
}

View file

@ -32,9 +32,9 @@ model User {
assignedTickets Ticket[] @relation("AssignedTickets")
reopenedTickets Ticket[] @relation("ReopenedTickets")
tagSubscriptions UserTagSubscription[]
createdCategoryTags CategoryTag[] @relation("CreatedCategoryTags")
helper Boolean @default(false)
tagSubscriptions UserTagSubscription[]
createdCategoryTags CategoryTag[] @relation("CreatedCategoryTags")
helper Boolean @default(false)
createdAt DateTime @default(now())
}
@ -75,7 +75,7 @@ model Ticket {
questionTag QuestionTag? @relation("QuestionTagTickets", fields: [questionTagId], references: [id])
questionTagId Int?
categoryTag CategoryTag? @relation("CategoryTagTickets", fields: [categoryTagId], references: [id])
categoryTag CategoryTag? @relation("CategoryTagTickets", fields: [categoryTagId], references: [id])
categoryTagId Int?
createdAt DateTime @default(now())
@ -90,8 +90,9 @@ model QuestionTag {
// These tags are team tags
model Tag {
id Int @id @unique @default(autoincrement())
name String @unique
id Int @id @unique @default(autoincrement())
name String @unique
description String?
ticketsOnTags TagsOnTickets[]
@ -134,11 +135,13 @@ model BotMessage {
}
model CategoryTag {
id Int @id @unique @default(autoincrement())
name String @unique
tickets Ticket[] @relation("CategoryTagTickets")
id Int @id @unique @default(autoincrement())
name String @unique
slug String? @unique
description String?
tickets Ticket[] @relation("CategoryTagTickets")
createdBy User? @relation("CreatedCategoryTags", fields: [createdById], references: [id])
createdBy User? @relation("CreatedCategoryTags", fields: [createdById], references: [id])
createdById Int?
createdAt DateTime @default(now())