From c2595b616b631fc7bcd8e235f7265bcfc0659569 Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Mon, 26 Aug 2024 17:44:23 +0200 Subject: [PATCH] Zulip auto post --- server/reflector/db/rooms.py | 16 +++ server/reflector/views/rooms.py | 31 +++++ www/.gitignore | 5 +- www/app/[domain]/rooms/page.tsx | 226 +++++++++++++++++++++++++++----- www/app/api/schemas.gen.ts | 60 ++++++++- www/app/api/services.gen.ts | 27 ++++ www/app/api/types.gen.ts | 33 +++++ www/package.json | 2 +- www/yarn.lock | 18 +-- 9 files changed, 373 insertions(+), 45 deletions(-) diff --git a/server/reflector/db/rooms.py b/server/reflector/db/rooms.py index 5cd33dd9..fa4eb2a7 100644 --- a/server/reflector/db/rooms.py +++ b/server/reflector/db/rooms.py @@ -68,6 +68,9 @@ class RoomController: self, name: str, user_id: str, + zulip_auto_post: bool, + zulip_stream: str, + zulip_topic: str, ): """ Add a new room @@ -75,11 +78,24 @@ class RoomController: room = Room( name=name, user_id=user_id, + zulip_auto_post=zulip_auto_post, + zulip_stream=zulip_stream, + zulip_topic=zulip_topic, ) query = rooms.insert().values(**room.model_dump()) await database.execute(query) return room + async def update(self, room: Room, values: dict, mutate=True): + """ + Update a room fields with key/values in values + """ + query = rooms.update().where(rooms.c.id == room.id).values(**values) + await database.execute(query) + if mutate: + for key, value in values.items(): + setattr(room, key, value) + async def get_by_id(self, room_id: str, **kwargs) -> Room | None: """ Get a room by id diff --git a/server/reflector/views/rooms.py b/server/reflector/views/rooms.py index 116183cd..7b94cd0b 100644 --- a/server/reflector/views/rooms.py +++ b/server/reflector/views/rooms.py @@ -21,6 +21,9 @@ class Room(BaseModel): name: str user_id: str created_at: datetime + zulip_auto_post: bool + zulip_stream: str + zulip_topic: str class Meeting(BaseModel): @@ -35,6 +38,16 @@ class Meeting(BaseModel): class CreateRoom(BaseModel): name: str + zulip_auto_post: bool + zulip_stream: str + zulip_topic: str + + +class UpdateRoom(BaseModel): + name: str + zulip_auto_post: bool + zulip_stream: str + zulip_topic: str class DeletionStatus(BaseModel): @@ -69,9 +82,27 @@ async def rooms_create( return await rooms_controller.add( name=room.name, user_id=user_id, + zulip_auto_post=room.zulip_auto_post, + zulip_stream=room.zulip_stream, + zulip_topic=room.zulip_topic, ) +@router.patch("/rooms/{room_id}", response_model=Room) +async def rooms_update( + room_id: str, + info: UpdateRoom, + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +): + user_id = user["sub"] if user else None + room = await rooms_controller.get_by_id_for_http(room_id, user_id=user_id) + if not room: + raise HTTPException(status_code=404, detail="Room not found") + values = info.dict(exclude_unset=True) + await rooms_controller.update(room, values) + return room + + @router.delete("/rooms/{room_id}", response_model=DeletionStatus) async def rooms_delete( room_id: str, diff --git a/www/.gitignore b/www/.gitignore index 895fbb29..8f6f9013 100644 --- a/www/.gitignore +++ b/www/.gitignore @@ -40,4 +40,7 @@ next-env.d.ts # Sentry Auth Token .sentryclirc -config.ts \ No newline at end of file +config.ts + +# openapi logs +openapi-ts-error-*.log diff --git a/www/app/[domain]/rooms/page.tsx b/www/app/[domain]/rooms/page.tsx index dc6c61ba..0ed6a119 100644 --- a/www/app/[domain]/rooms/page.tsx +++ b/www/app/[domain]/rooms/page.tsx @@ -1,7 +1,6 @@ "use client"; import { - Box, Button, Card, CardBody, @@ -9,7 +8,6 @@ import { FormControl, FormHelperText, FormLabel, - Grid, Heading, Input, Link, @@ -29,47 +27,145 @@ import { MenuButton, MenuList, MenuItem, - AlertDialog, IconButton, + Checkbox, } from "@chakra-ui/react"; -import NextLink from "next"; -import React, { ReactNode, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { Container } from "@chakra-ui/react"; -import { PlusSquareIcon } from "@chakra-ui/icons"; +import { FaEllipsisVertical, FaTrash, FaPencil } from "react-icons/fa6"; import useApi from "../../lib/useApi"; import useRoomList from "./useRoomList"; -import { FaEllipsisVertical, FaTrash } from "react-icons/fa6"; -import next from "next"; +import { DomainContext } from "../domainContext"; +import { Select, Options, OptionBase } from "chakra-react-select"; + +interface Stream { + id: number; + name: string; + topics: string[]; +} + +interface SelectOption extends OptionBase { + label: string; + value: string; +} export default function RoomsList() { const { isOpen, onOpen, onClose } = useDisclosure(); - const [roomName, setRoomName] = useState(""); + const [room, setRoom] = useState({ + name: "", + zulipAutoPost: false, + zulipStream: "", + zulipTopic: "", + }); + const [isEditing, setIsEditing] = useState(false); + const [editRoomId, setEditRoomId] = useState(""); const api = useApi(); const [page, setPage] = useState(1); const { loading, response, refetch } = useRoomList(page); + const [streams, setStreams] = useState([]); - const handleAddRoom = async () => { + const { zulip_streams } = useContext(DomainContext); + + useEffect(() => { + const fetchZulipStreams = async () => { + try { + const response = await fetch(zulip_streams + "/streams.json"); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + let data = await response.json(); + data = data.sort((a: Stream, b: Stream) => + a.name.localeCompare(b.name), + ); + setStreams(data); + } catch (err) { + console.error("Error fetching streams:", err); + } + }; + + if (room.zulipAutoPost) { + fetchZulipStreams(); + } + }, [room.zulipAutoPost]); + + const streamOptions: Options = streams.map((stream) => { + return { label: stream.name, value: stream.name }; + }); + + const topicOptions = + streams + .find((stream) => stream.name === room.zulipStream) + ?.topics.map((topic) => ({ label: topic, value: topic })) || []; + + const handleSaveRoom = async () => { try { - const response = await api?.v1RoomsCreate({ - requestBody: { name: roomName }, + console.log(room); + if (isEditing) { + await api?.v1RoomsUpdate({ + roomId: editRoomId, + requestBody: { + name: room.name, + zulip_auto_post: room.zulipAutoPost, + zulip_stream: room.zulipStream, + zulip_topic: room.zulipTopic, + }, + }); + } else { + await api?.v1RoomsCreate({ + requestBody: { + name: room.name, + zulip_auto_post: room.zulipAutoPost, + zulip_stream: room.zulipStream, + zulip_topic: room.zulipTopic, + }, + }); + } + setRoom({ + name: "", + zulipAutoPost: false, + zulipStream: "", + zulipTopic: "", }); - setRoomName(""); + setIsEditing(false); + setEditRoomId(""); refetch(); } catch (err) {} onClose(); }; + const handleEditRoom = (roomId, roomData) => { + setRoom({ + name: roomData.name, + zulipAutoPost: roomData.zulip_auto_post, + zulipStream: roomData.zulip_stream, + zulipTopic: roomData.zulip_topic, + }); + setEditRoomId(roomId); + setIsEditing(true); + onOpen(); + }; + const handleDeleteRoom = async (roomId: string) => { try { - const response = await api?.v1RoomsDelete({ + await api?.v1RoomsDelete({ roomId, }); refetch(); } catch (err) {} }; - const handleRoomNameChange = (e) => { - setRoomName(e.target.value); + const handleRoomChange = (e) => { + let { name, value, type, checked } = e.target; + if (name === "name") { + value = value + .replace(/[^a-zA-Z0-9\s-]/g, "") + .replace(/\s+/g, "-") + .toLowerCase(); + } + setRoom({ + ...room, + [name]: type === "checkbox" ? checked : value, + }); }; if (loading && !response) @@ -91,23 +187,81 @@ export default function RoomsList() { > Rooms - - Add Room + {isEditing ? "Edit Room" : "Add Room"} Room name + + No spaces or special characters allowed + + + + + + Automatically post transcription to Zulip + + + + Zulip stream + + setRoom({ + ...room, + zulipTopic: newValue!.value, + }) + } + isDisabled={!room.zulipAutoPost} /> - Please enter room name @@ -116,8 +270,14 @@ export default function RoomsList() { Cancel - @@ -126,17 +286,13 @@ export default function RoomsList() { {response?.items && response.items.length > 0 ? ( - response.items.map((room) => ( - + response.items.map((roomData) => ( + - - {room.name} + + {roomData.name} @@ -148,7 +304,13 @@ export default function RoomsList() { /> handleDeleteRoom(room.id)} + onClick={() => handleEditRoom(roomData.id, roomData)} + icon={} + > + Edit + + handleDeleteRoom(roomData.id)} icon={} > Delete diff --git a/www/app/api/schemas.gen.ts b/www/app/api/schemas.gen.ts index 7aac08d7..baa77150 100644 --- a/www/app/api/schemas.gen.ts +++ b/www/app/api/schemas.gen.ts @@ -59,9 +59,21 @@ export const $CreateRoom = { type: "string", title: "Name", }, + zulip_auto_post: { + type: "boolean", + title: "Zulip Auto Post", + }, + zulip_stream: { + type: "string", + title: "Zulip Stream", + }, + zulip_topic: { + type: "string", + title: "Zulip Topic", + }, }, type: "object", - required: ["name"], + required: ["name", "zulip_auto_post", "zulip_stream", "zulip_topic"], title: "CreateRoom", } as const; @@ -643,9 +655,29 @@ export const $Room = { format: "date-time", title: "Created At", }, + zulip_auto_post: { + type: "boolean", + title: "Zulip Auto Post", + }, + zulip_stream: { + type: "string", + title: "Zulip Stream", + }, + zulip_topic: { + type: "string", + title: "Zulip Topic", + }, }, type: "object", - required: ["id", "name", "user_id", "created_at"], + required: [ + "id", + "name", + "user_id", + "created_at", + "zulip_auto_post", + "zulip_stream", + "zulip_topic", + ], title: "Room", } as const; @@ -807,6 +839,30 @@ export const $UpdateParticipant = { title: "UpdateParticipant", } as const; +export const $UpdateRoom = { + properties: { + name: { + type: "string", + title: "Name", + }, + zulip_auto_post: { + type: "boolean", + title: "Zulip Auto Post", + }, + zulip_stream: { + type: "string", + title: "Zulip Stream", + }, + zulip_topic: { + type: "string", + title: "Zulip Topic", + }, + }, + type: "object", + required: ["name", "zulip_auto_post", "zulip_stream", "zulip_topic"], + title: "UpdateRoom", +} as const; + export const $UpdateTranscript = { properties: { name: { diff --git a/www/app/api/services.gen.ts b/www/app/api/services.gen.ts index ec677555..1bd59275 100644 --- a/www/app/api/services.gen.ts +++ b/www/app/api/services.gen.ts @@ -8,6 +8,8 @@ import type { V1RoomsListResponse, V1RoomsCreateData, V1RoomsCreateResponse, + V1RoomsUpdateData, + V1RoomsUpdateResponse, V1RoomsDeleteData, V1RoomsDeleteResponse, V1RoomsCreateMeetingData, @@ -120,6 +122,31 @@ export class DefaultService { }); } + /** + * Rooms Update + * @param data The data for the request. + * @param data.roomId + * @param data.requestBody + * @returns Room Successful Response + * @throws ApiError + */ + public v1RoomsUpdate( + data: V1RoomsUpdateData, + ): CancelablePromise { + return this.httpRequest.request({ + method: "PATCH", + url: "/v1/rooms/{room_id}", + path: { + room_id: data.roomId, + }, + body: data.requestBody, + mediaType: "application/json", + errors: { + 422: "Validation Error", + }, + }); + } + /** * Rooms Delete * @param data The data for the request. diff --git a/www/app/api/types.gen.ts b/www/app/api/types.gen.ts index fcf77c05..ca6d6327 100644 --- a/www/app/api/types.gen.ts +++ b/www/app/api/types.gen.ts @@ -16,6 +16,9 @@ export type CreateParticipant = { export type CreateRoom = { name: string; + zulip_auto_post: boolean; + zulip_stream: string; + zulip_topic: string; }; export type CreateTranscript = { @@ -126,6 +129,9 @@ export type Room = { name: string; user_id: string; created_at: string; + zulip_auto_post: boolean; + zulip_stream: string; + zulip_topic: string; }; export type RtcOffer = { @@ -165,6 +171,13 @@ export type UpdateParticipant = { name?: string | null; }; +export type UpdateRoom = { + name: string; + zulip_auto_post: boolean; + zulip_stream: string; + zulip_topic: string; +}; + export type UpdateTranscript = { name?: string | null; locked?: boolean | null; @@ -216,6 +229,13 @@ export type V1RoomsCreateData = { export type V1RoomsCreateResponse = Room; +export type V1RoomsUpdateData = { + requestBody: UpdateRoom; + roomId: string; +}; + +export type V1RoomsUpdateResponse = Room; + export type V1RoomsDeleteData = { roomId: string; }; @@ -426,6 +446,19 @@ export type $OpenApiTs = { }; }; "/v1/rooms/{room_id}": { + patch: { + req: V1RoomsUpdateData; + res: { + /** + * Successful Response + */ + 200: Room; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; delete: { req: V1RoomsDeleteData; res: { diff --git a/www/package.json b/www/package.json index 7d5afbb6..7909cad0 100644 --- a/www/package.json +++ b/www/package.json @@ -27,7 +27,7 @@ "@whereby.com/browser-sdk": "^3.3.4", "autoprefixer": "10.4.14", "axios": "^1.6.2", - "chakra-react-select": "^4.7.6", + "chakra-react-select": "^4.9.1", "eslint": "^8.56.0", "eslint-config-next": "^14.0.4", "fontawesome": "^5.6.3", diff --git a/www/yarn.lock b/www/yarn.lock index 09d98356..21406fab 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -2347,12 +2347,12 @@ caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.300015 resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz" integrity sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw== -chakra-react-select@^4.7.6: - version "4.7.6" - resolved "https://registry.yarnpkg.com/chakra-react-select/-/chakra-react-select-4.7.6.tgz#3be8ffb314b8e75ef02663fd3e2fdf872b79683b" - integrity sha512-ZL43hyXPnWf1g/HjsZDecbeJ4F2Q6tTPYJozlKWkrQ7lIX7ORP0aZYwmc5/Wly4UNzMimj2Vuosl6MmIXH+G2g== +chakra-react-select@^4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/chakra-react-select/-/chakra-react-select-4.9.1.tgz#38e421a0400c26e7f25d3dd28e6b93a021f08b77" + integrity sha512-jmgfN+S/wnTaCp3pW30GYDIZ5J8jWcT1gIbhpw6RdKV+atm/U4/sT+gaHOHHhRL8xeaYip+iI/m8MPGREkve0w== dependencies: - react-select "5.7.7" + react-select "5.8.0" chalk@3.0.0: version "3.0.0" @@ -5116,10 +5116,10 @@ react-select-search@^4.1.7: resolved "https://registry.yarnpkg.com/react-select-search/-/react-select-search-4.1.7.tgz#5662729b9052282bde52e1352006d495d9c5ed6e" integrity sha512-pU7ONAdK+bmz2tbhBWYQv9m5mnXOn8yImuiy+5UhimIG80d5iKv3nSYJIjJWjDbdrrdoXiCRwQm8xbA8llTjmQ== -react-select@5.7.7: - version "5.7.7" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.7.tgz#dbade9dbf711ef2a181970c10f8ab319ac37fbd0" - integrity sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw== +react-select@5.8.0: + version "5.8.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.8.0.tgz#bd5c467a4df223f079dd720be9498076a3f085b5" + integrity sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA== dependencies: "@babel/runtime" "^7.12.0" "@emotion/cache" "^11.4.0"