Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions api/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ type ApplicationParams struct {
//
// example: 5
DefaultPriority int `form:"defaultPriority" query:"defaultPriority" json:"defaultPriority"`
// The order in which this application should appear in the UI. Defaults to 0.
//
// example: 7
SortOrder int `form:"sortOrder" query:"sortOrder" json:"sortOrder"`
}

// CreateApplication creates an application and returns the access token.
Expand Down Expand Up @@ -90,6 +94,7 @@ func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) {
Name: applicationParams.Name,
Description: applicationParams.Description,
DefaultPriority: applicationParams.DefaultPriority,
SortOrder: applicationParams.SortOrder,
Token: auth.GenerateNotExistingToken(generateApplicationToken, a.applicationExists),
UserID: auth.GetUserID(ctx),
Internal: false,
Expand Down Expand Up @@ -251,6 +256,7 @@ func (a *ApplicationAPI) UpdateApplication(ctx *gin.Context) {
app.Description = applicationParams.Description
app.Name = applicationParams.Name
app.DefaultPriority = applicationParams.DefaultPriority
app.SortOrder = applicationParams.SortOrder

if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success {
return
Expand Down Expand Up @@ -466,3 +472,99 @@ func ValidApplicationImageExt(ext string) bool {
return false
}
}

// ApplicationOrderParams represents the new order for applications
type ApplicationOrderParams struct {
// Array of application IDs in the desired order
// required: true
// example: [3, 1, 5, 2, 4]
ApplicationIDs []uint `json:"applicationIds" binding:"required"`
}

// ReorderApplications updates the sort order of multiple applications based on their position in the array.
// swagger:operation PUT /application/reorder application reorderApplications
//
// Reorder applications.
//
// ---
// consumes: [application/json]
// produces: [application/json]
// security: [clientTokenAuthorizationHeader: [], clientTokenHeader: [], clientTokenQuery: [], basicAuth: []]
// parameters:
// - name: body
// in: body
// description: ordered list of application IDs
// required: true
// schema:
// $ref: "#/definitions/ApplicationOrderParams"
// responses:
// 200:
// description: Ok
// schema:
// type: array
// items:
// $ref: "#/definitions/Application"
// 400:
// description: Bad Request
// schema:
// $ref: "#/definitions/Error"
// 401:
// description: Unauthorized
// schema:
// $ref: "#/definitions/Error"
// 403:
// description: Forbidden
// schema:
// $ref: "#/definitions/Error"
// 500:
// description: Server Error
// schema:
// $ref: "#/definitions/Error"
func (a *ApplicationAPI) ReorderApplications(ctx *gin.Context) {
userID := auth.GetUserID(ctx)

orderParams := ApplicationOrderParams{}
if err := ctx.Bind(&orderParams); err != nil {
ctx.AbortWithError(400, err)
return
}

if len(orderParams.ApplicationIDs) == 0 {
ctx.AbortWithError(400, errors.New("applicationIds array cannot be empty"))
return
}

userApps, err := a.DB.GetApplicationsByUser(userID)
if success := successOrAbort(ctx, 500, err); !success {
return
}

userAppMap := make(map[uint]bool)
for _, app := range userApps {
userAppMap[app.ID] = true
}

for _, appID := range orderParams.ApplicationIDs {
if !userAppMap[appID] {
ctx.AbortWithError(403, fmt.Errorf("application with id %d does not belong to user", appID))
return
}
}

updatedApps := make([]*model.Application, 0, len(orderParams.ApplicationIDs))
for i, appID := range orderParams.ApplicationIDs {
app, err := a.DB.GetApplicationByID(appID)
if success := successOrAbort(ctx, 500, err); !success {
return
}
if app != nil {
app.SortOrder = i
if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success {
return
}
updatedApps = append(updatedApps, withResolvedImage(app))
}
}

ctx.JSON(200, updatedApps)
}
3 changes: 2 additions & 1 deletion api/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ func (s *ApplicationSuite) Test_ensureApplicationHasCorrectJsonRepresentation()
Image: "asd",
Internal: true,
LastUsed: nil,
SortOrder: 7,
}
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc", "image": "asd", "internal":true, "defaultPriority":0, "lastUsed":null}`)
test.JSONEquals(s.T(), actual, `{"id":1,"token":"Aasdasfgeeg","name":"myapp","description":"mydesc", "image": "asd", "internal":true, "defaultPriority":0, "lastUsed":null, "sortOrder":7}`)
}

func (s *ApplicationSuite) Test_CreateApplication_expectBadRequestOnEmptyName() {
Expand Down
2 changes: 1 addition & 1 deletion database/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (d *GormDatabase) DeleteApplicationByID(id uint) error {
// GetApplicationsByUser returns all applications from a user.
func (d *GormDatabase) GetApplicationsByUser(userID uint) ([]*model.Application, error) {
var apps []*model.Application
err := d.DB.Where("user_id = ?", userID).Order("id ASC").Find(&apps).Error
err := d.DB.Where("user_id = ?", userID).Order("sort_order ASC").Find(&apps).Error
if err == gorm.ErrRecordNotFound {
err = nil
}
Expand Down
4 changes: 4 additions & 0 deletions model/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,8 @@ type Application struct {
// read only: true
// example: 2019-01-01T00:00:00Z
LastUsed *time.Time `json:"lastUsed"`
// The order in which the application should appear in the UI.
//
// example: 7
SortOrder int `json:"sortOrder" query:"sortOrder" form: "sortOrder"`
}
2 changes: 2 additions & 0 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co

app.POST("", applicationHandler.CreateApplication)

app.PUT("/reorder", applicationHandler.ReorderApplications)

app.POST("/:id/image", applicationHandler.UploadApplicationImage)

app.DELETE("/:id/image", applicationHandler.RemoveApplicationImage)
Expand Down
3 changes: 3 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"homepage": ".",
"proxy": "http://localhost:80",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.2.0",
Expand Down
13 changes: 11 additions & 2 deletions ui/src/application/AddApplicationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ import React, {useState} from 'react';

interface IProps {
fClose: VoidFunction;
fOnSubmit: (name: string, description: string, defaultPriority: number) => Promise<void>;
fOnSubmit: (name: string, description: string, defaultPriority: number, sortOrder: number) => Promise<void>;
}

export const AddApplicationDialog = ({fClose, fOnSubmit}: IProps) => {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [defaultPriority, setDefaultPriority] = useState(0);
const [sortOrder, setSortOrder] = useState(0);

const submitEnabled = name.length !== 0;
const submitAndClose = async () => {
await fOnSubmit(name, description, defaultPriority);
await fOnSubmit(name, description, defaultPriority, sortOrder);
fClose();
};

Expand Down Expand Up @@ -57,6 +58,14 @@ export const AddApplicationDialog = ({fClose, fOnSubmit}: IProps) => {
onChange={(value) => setDefaultPriority(value)}
fullWidth
/>
<NumberField
margin="dense"
className="sortOrder"
label="Sort Order"
value={sortOrder}
onChange={(value) => setSortOrder(value)}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>
Expand Down
16 changes: 14 additions & 2 deletions ui/src/application/AppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ export class AppStore extends BaseStore<IApplication> {
id: number,
name: string,
description: string,
defaultPriority: number
defaultPriority: number,
sortOrder: number
): Promise<void> => {
await axios.put(`${config.get('url')}application/${id}`, {
name,
description,
defaultPriority,
sortOrder,
});
await this.refresh();
this.snack('Application updated');
Expand All @@ -57,17 +59,27 @@ export class AppStore extends BaseStore<IApplication> {
public create = async (
name: string,
description: string,
defaultPriority: number
defaultPriority: number,
sortOrder: number
): Promise<void> => {
await axios.post(`${config.get('url')}application`, {
name,
description,
defaultPriority,
sortOrder,
});
await this.refresh();
this.snack('Application created');
};

public reorder = async (applicationIds: number[]): Promise<void> => {
await axios.put(`${config.get('url')}application/reorder`, {
applicationIds,
});
await this.refresh();
this.snack('Applications reordered');
};

public getName = (id: number): string => {
const app = this.getByIDOrUndefined(id);
return id === -1 ? 'All Messages' : app !== undefined ? app.name : 'unknown';
Expand Down
Loading