preparations for thread add and edit

This commit is contained in:
overflowerror 2021-08-15 22:45:24 +02:00
parent 20e832e9e1
commit a98a95f311
15 changed files with 257 additions and 23 deletions

View file

@ -21,6 +21,7 @@ type Data interface {
AddAccount(account *models.Account) error
UpdateAccount(account *models.Account) error
AddThread(thread *models.Thread) error
GetScheduledThreads() ([]models.Thread, error)
GetTweetsForThread(thread *models.Thread) ([]models.Tweet, error)
UpdateThread(thread *models.Thread) error

View file

@ -12,4 +12,6 @@ type Logic interface {
GetAccounts(user *models.User) ([]models.Account, error)
AddAccount(user *models.User) (string, *url.URL, error)
AddAccountResolve(user *models.User, id string, pin string) (*models.Account, error)
AddThread(thread *models.Thread, user *models.User) error
}

View file

@ -17,7 +17,7 @@ const (
type Thread struct {
BaseModel
AccountID uuid.UUID `json:"-"`
Account *Account `json:"-"`
Account *Account `json:"account"`
Tweets []Tweet `json:"tweets"`
ScheduledFor time.Time `json:"scheduled_for"`

View file

@ -33,3 +33,10 @@ func (d *Data) GetScheduledThreads() ([]models.Thread, error) {
Error
return threads, err
}
func (d *Data) AddThread(thread *models.Thread) error {
return d.db.
Omit("Account").
Create(thread).
Error
}

View file

@ -7,4 +7,5 @@ var (
ErrInvalidSession = errors.New("invalid session")
ErrInternalError = errors.New("something went wrong")
ErrInvalidParameter = errors.New("invalid parameter")
ErrNotFound = errors.New("resource not found")
)

View file

@ -0,0 +1,26 @@
package logic
import (
uuid "github.com/satori/go.uuid"
"threadule/backend/internal/data/models"
)
func (l *Logic) AddThread(thread *models.Thread, user *models.User) error {
if uuid.Nil == thread.AccountID {
if thread.Account == nil {
return ErrInvalidParameter
}
if uuid.Nil == thread.Account.ID {
return ErrInvalidParameter
}
thread.AccountID = thread.Account.ID
}
_, err := l.ctx.Data.GetAccountById(user, thread.AccountID.String())
if err != nil {
return ErrNotFound
}
err = l.ctx.Data.AddThread(thread)
return err
}

View file

@ -24,6 +24,8 @@ func StatusResponse(ctx *web.Context, status int, details string) {
func ErrorResponse(ctx *web.Context, err error) {
switch err {
case logic.ErrNotFound:
StatusResponse(ctx, http.StatusNotFound, err.Error())
case logic.ErrInvalidParameter:
StatusResponse(ctx, http.StatusBadRequest, err.Error())
case logic.ErrLoginFailed:

View file

@ -0,0 +1,27 @@
package presentation
import (
"threadule/backend/internal/data/models"
"threadule/backend/internal/web"
)
func AddThread(ctx *web.Context) {
var thread models.Thread
err := ctx.ReadJSON(&thread)
if err != nil {
ErrorResponse(ctx, err)
return
}
err = ctx.AppCtx.Logic.AddThread(&thread, ctx.Session.User)
if err != nil {
ErrorResponse(ctx, err)
return
}
err = ctx.WriteJSON(&thread)
if err != nil {
ErrorResponse(ctx, err)
return
}
}

View file

@ -27,5 +27,7 @@ func Setup(ctx *app.Context) http.Handler {
router.POST("/account/", authenticated(AddAccount))
router.POST("/account/:id", authenticated(AddAccountResolve))
router.POST("/thread/", authenticated(AddThread))
return router
}

View file

@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",

View file

@ -0,0 +1,18 @@
import {Client} from "../client";
import Endpoint from "./Endpoint";
import Thread from "../entities/Thread";
const API_PREFIX = "/api/account/"
class ThreadEndpoint extends Endpoint {
constructor(client: Client) {
super(client)
this.requireAuthenticated()
}
public async add(thread: Thread): Promise<Thread> {
return await this.post<Thread, Thread>(API_PREFIX, thread)
}
}
export default ThreadEndpoint

View file

@ -1,3 +1,4 @@
import Tweet from "./Tweet";
export enum ThreadStatus {
SCHEDULED = "SCHEDULED",
@ -8,10 +9,10 @@ export enum ThreadStatus {
type Thread = {
id: string,
//tweets: Tweet[],
tweets: Tweet[],
scheduled_for: Date,
status: ThreadStatus,
error: string|null
error: string|null,
}
export default Thread

View file

@ -0,0 +1,16 @@
export enum TweetStatus {
SCHEDULED = "SCHEDULED",
FAILED = "FAILED",
DONE = "DONE",
}
type Tweet = {
id: string,
text: string,
status: TweetStatus,
tweet_id: number|null,
error: string|null
}
export default Tweet

View file

@ -1,37 +1,68 @@
import {FunctionComponent} from "react";
import {FunctionComponent, useState} from "react";
import {Avatar, Card, CardActions, CardContent, CardHeader, IconButton} from "@material-ui/core";
import Account from "../../api/entities/Account";
import ThreadList from "../ThreadList";
import AddIcon from '@material-ui/icons/Add';
import ThreadFormDialog from "../ThreadFormDialog";
import Thread, {ThreadStatus} from "../../api/entities/Thread";
export type AccountCardProps = {
account: Account
}
const emptyThread = () => ({
id: "",
scheduled_for: new Date(),
status: ThreadStatus.SCHEDULED,
tweets: [],
error: null,
})
const AccountCard: FunctionComponent<AccountCardProps> = ({account}) => {
const [editThread, setEditThread] = useState<Thread | null>(null)
const openNewForm = () => {
setEditThread(emptyThread())
}
return (
<Card>
<CardHeader
avatar={
<Avatar alt={account.screen_name} src={account.avatar_url}/>
}
action={
<IconButton aria-label="settings">
<>
<Card>
<CardHeader
avatar={
<Avatar alt={account.screen_name} src={account.avatar_url}/>
}
action={
<IconButton aria-label="settings">
</IconButton>
}
title={account.name}
subheader={account.screen_name}
/>
<CardContent>
<ThreadList threads={account.threads}/>
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add" onClick={() => {
openNewForm()
}}>
<AddIcon/>
</IconButton>
}
title={account.name}
subheader={account.screen_name}
</CardActions>
</Card>
<ThreadFormDialog
account={account}
open={Boolean(editThread)}
initial={editThread ? editThread : emptyThread()}
onSubmit={(thread) => {
account.threads.push(thread)
setEditThread(null)
}}
/>
<CardContent>
<ThreadList threads={account.threads} />
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add">
<AddIcon/>
</IconButton>
</CardActions>
</Card>
</>
)
}

View file

@ -0,0 +1,99 @@
import {FunctionComponent, useState} from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
IconButton,
TextField
} from "@material-ui/core";
import Account from "../../api/entities/Account";
import Thread from "../../api/entities/Thread";
import AddIcon from "@material-ui/icons/Add";
import {TweetStatus} from "../../api/entities/Tweet";
export type ThreadFormProps = {
open: boolean
account: Account
initial: Thread
onSubmit: (thread: Thread) => void
}
const Index: FunctionComponent<ThreadFormProps> = ({open, account, initial, onSubmit}) => {
const [thread, setThread] = useState<Thread>(initial)
return (
<Dialog open={open}>
<DialogTitle title={"Thread"}/>
<DialogContent>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
id="datetime-local"
label="Scheduled For"
type="datetime-local"
value={thread.scheduled_for.toISOString().replace(/:[0-9]{2}\.[0-9]{3}.*/, "")}
onChange={event => {
setThread({
...thread,
scheduled_for: new Date(event.target.value)
})
}}
InputLabelProps={{
shrink: true,
}}
/>
</Grid>
{
thread.tweets.map((tweet, index) => (
<Grid item xs={12}>
<TextField
id="outlined-multiline-static"
label={"Tweet " + (index + 1)}
multiline
rows={3}
value={tweet.text}
onChange={event => {
thread.tweets[index].text = event.target.value
setThread({
...thread
})
}}
variant="outlined"
fullWidth
/>
</Grid>
))
}
<Grid item xs={12}>
<IconButton aria-label="add" onClick={() => {
thread.tweets.push({
id: "",
text: "",
status: TweetStatus.SCHEDULED,
tweet_id: null,
error: null
})
setThread({
...thread
})
}}>
<AddIcon/>
</IconButton>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={() => {
onSubmit(thread)
}} color="primary">
Save changes
</Button>
</DialogActions>
</Dialog>
)
}
export default Index