channel editing

This commit is contained in:
Jake Potrebic 2021-03-20 13:45:16 -07:00
parent 1522489435
commit 1bbe90ff99
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
13 changed files with 180 additions and 22 deletions

View File

@ -70,7 +70,6 @@ export default class ChannelModal extends HangarFormModal {
name: '',
color: '',
nonReviewed: false,
temp: true,
versionCount: 0,
};

View File

@ -352,6 +352,7 @@ const msgs: LocaleMessageObject = {
maxChannels: 'This project already has the maximum number of channels: {0}',
duplicateColor: 'This project already has a channel with this color',
duplicateName: 'This project already has a channel with this name',
tooLongName: 'Channel name is too long',
},
},
manage: {

View File

@ -25,7 +25,9 @@
<td>
<ChannelModal :edit="true" :channel="channel" @create="editChannel">
<template #activator="{ on, attrs }">
<v-btn small color="warning" v-bind="attrs" v-on="on">{{ $t('channel.manage.editButton') }}</v-btn>
<v-btn small color="warning" v-bind="attrs" v-on="on">
{{ $t('channel.manage.editButton') }}
</v-btn>
</template>
</ChannelModal>
</td>
@ -40,7 +42,14 @@
<v-card-actions>
<ChannelModal @create="addChannel">
<template #activator="{ on, attrs }">
<v-btn v-if="channels.length < validations.project.maxChannelCount" color="primary" v-bind="attrs" v-on="on">
<v-btn
v-if="channels.length < validations.project.maxChannelCount"
:loading="loading.add"
:disabled="channels.length >= validations.project.maxChannelCount"
color="primary"
v-bind="attrs"
v-on="on"
>
<v-icon left>mdi-plus</v-icon>
{{ $t('channel.manage.add') }}
</v-btn>
@ -65,10 +74,24 @@ import { RootState } from '~/store';
})
export default class ProjectChannelsPage extends HangarProjectMixin {
channels!: ProjectChannel[];
loading = {
add: false,
};
// TODO editChannel
editChannel(name: String) {
console.log('edit channel ', name);
editChannel(channel: ProjectChannel) {
if (!channel.id) return;
const id = channel.id;
this.$api
.requestInternal(`channels/${this.project.id}/edit`, true, 'post', {
id,
name: channel.name,
color: channel.color,
nonReviewed: channel.nonReviewed,
})
.then(() => {
this.$nuxt.refresh();
})
.catch(this.$util.handleRequestError);
}
// TODO deleteChannel
@ -76,9 +99,21 @@ export default class ProjectChannelsPage extends HangarProjectMixin {
console.log('delete channel ', name);
}
// TODO addChannel
addChannel(channel: ProjectChannel) {
this.channels.push(Object.assign({}, channel));
this.loading.add = true;
this.$api
.requestInternal(`channels/${this.project.id}/create`, true, 'post', {
name: channel.name,
color: channel.color,
nonReviewed: channel.nonReviewed,
})
.then(() => {
this.$nuxt.refresh();
})
.catch(this.$util.handleRequestError)
.finally(() => {
this.loading.add = false;
});
}
async asyncData({ $api, $util, params }: Context) {

View File

@ -40,6 +40,7 @@ import { Component, Vue } from 'nuxt-property-decorator';
import { Flag } from 'hangar-internal';
import UserAvatar from '~/components/users/UserAvatar.vue';
import { Visibility } from '~/types/enums';
@Component({
components: { UserAvatar },
})
@ -59,7 +60,7 @@ export default class AdminFlagsPage extends Vue {
];
get visibilities(): Visibility[] {
return Object.keys(Visibility);
return Object.keys(Visibility) as Visibility[];
}
// todo send to server

View File

@ -1,5 +1,6 @@
declare module 'hangar-internal' {
import { DependencyVersion, FileInfo, Named, ProjectNamespace, Version } from 'hangar-api';
import { Table } from 'hangar-internal';
import { Platform } from '~/types/enums';
interface PlatformDependency {
@ -24,7 +25,7 @@ declare module 'hangar-internal' {
isFile: boolean;
}
interface ProjectChannel extends Named {
interface ProjectChannel extends Named, Partial<Table> {
color: string;
nonReviewed: boolean;
temp?: boolean;

View File

@ -1,10 +1,15 @@
package io.papermc.hangar.controller.internal;
import io.papermc.hangar.controller.HangarController;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.common.NamedPermission;
import io.papermc.hangar.model.common.PermissionType;
import io.papermc.hangar.model.db.projects.ProjectTable;
import io.papermc.hangar.model.internal.api.requests.projects.ChannelForm;
import io.papermc.hangar.model.internal.api.requests.projects.EditChannelForm;
import io.papermc.hangar.model.internal.projects.HangarChannel;
import io.papermc.hangar.security.annotations.Anyone;
import io.papermc.hangar.security.annotations.permission.PermissionRequired;
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired;
import io.papermc.hangar.security.annotations.visibility.VisibilityRequired.Type;
import io.papermc.hangar.service.internal.projects.ChannelService;
@ -16,11 +21,14 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.validation.Valid;
import java.util.List;
@Anyone
@Controller
@RequestMapping(value = "/api/internal/channels", produces = MediaType.APPLICATION_JSON_VALUE)
public class ChannelController extends HangarController {
@ -34,13 +42,27 @@ public class ChannelController extends HangarController {
this.projectService = projectService;
}
@Anyone
@VisibilityRequired(type = Type.PROJECT, args = "{#author, #slug}")
@GetMapping(path = "/{author}/{slug}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<HangarChannel>> getProjectChannels(@PathVariable String author, @PathVariable String slug) {
public ResponseEntity<List<HangarChannel>> getChannels(@PathVariable String author, @PathVariable String slug) {
ProjectTable projectTable = projectService.getProjectTable(author, slug);
if (projectTable == null) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(channelService.getProjectChannels(projectTable.getId()));
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(type = PermissionType.PROJECT, perms = NamedPermission.EDIT_TAGS, args = "{#projectId}")
@PostMapping(path = "/{projectId}/create", consumes = MediaType.APPLICATION_JSON_VALUE)
public void createChannel(@PathVariable long projectId, @Valid @RequestBody ChannelForm channelForm) {
channelService.createProjectChannel(channelForm.getName(), channelForm.getColor(), projectId, channelForm.isNonReviewed());
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(type = PermissionType.PROJECT, perms = NamedPermission.EDIT_TAGS, args = "{#projectId}")
@PostMapping(path = "/{projectId}/edit", consumes = MediaType.APPLICATION_JSON_VALUE)
public void editChannel(@PathVariable long projectId, @Valid @RequestBody EditChannelForm channelForm) {
channelService.editProjectChannel(channelForm.getId(), channelForm.getName(), channelForm.getColor(), projectId, channelForm.isNonReviewed());
}
}

View File

@ -104,7 +104,8 @@ public interface HangarProjectsDAO {
@SqlQuery("SELECT pc.*," +
" (SELECT count(*) FROM project_versions pv WHERE pv.channel_id = pc.id) as version_count" +
" FROM project_channels pc" +
" WHERE pc.project_id = :projectId")
" WHERE pc.project_id = :projectId" +
" ORDER BY pc.created_at")
List<HangarChannel> getHangarChannels(long projectId);
@SqlUpdate("REFRESH MATERIALIZED VIEW home_projects")

View File

@ -22,12 +22,18 @@ public interface ProjectChannelsDAO {
@SqlUpdate("INSERT INTO project_channels (created_at, name, color, project_id, non_reviewed) VALUES (:now, :name, :color, :projectId, :nonReviewed)")
ProjectChannelTable insert(@BindBean ProjectChannelTable projectChannelTable);
@SqlUpdate("UPDATE project_channels SET name = :name, color = :color, non_reviewed = :nonReviewed WHERE id = :id")
void update(@BindBean ProjectChannelTable projectChannelTable);
@SqlQuery("SELECT * FROM project_channels WHERE project_id = :projectId")
List<ProjectChannelTable> getProjectChannels(long projectId);
@SqlQuery("SELECT * FROM project_channels WHERE project_id = :projectId AND name = :name AND color = :color")
ProjectChannelTable getProjectChannel(long projectId, String name, @EnumByOrdinal Color color);
@SqlQuery("SELECT * FROM project_channels WHERE id = :channelId")
ProjectChannelTable getProjectChannel(long channelId);
@SqlQuery("SELECT pc.* FROM project_channels pc JOIN project_versions pv ON pc.id = pv.channel_id WHERE pv.id = :versionId")
ProjectChannelTable getProjectChannelForVersion(long versionId);

View File

@ -0,0 +1,47 @@
package io.papermc.hangar.model.internal.api.requests.projects;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.papermc.hangar.controller.validations.Validate;
import io.papermc.hangar.model.common.Color;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public class ChannelForm {
@NotBlank
@Validate(SpEL = "@validations.regex(#root, @hangarConfig.channels.nameRegex)", message = "channel.modal.error.invalidName")
@Validate(SpEL = "@validations.max(#root, @hangarConfig.channels.maxNameLen)", message = "channel.modal.error.tooLongName")
private final String name;
@NotNull
private final Color color;
private final boolean nonReviewed;
@JsonCreator
public ChannelForm(String name, Color color, boolean nonReviewed) {
this.name = name;
this.color = color;
this.nonReviewed = nonReviewed;
}
public String getName() {
return name;
}
public Color getColor() {
return color;
}
public boolean isNonReviewed() {
return nonReviewed;
}
@Override
public String toString() {
return "ChannelForm{" +
"name='" + name + '\'' +
", color=" + color +
", nonReviewed=" + nonReviewed +
'}';
}
}

View File

@ -0,0 +1,24 @@
package io.papermc.hangar.model.internal.api.requests.projects;
import io.papermc.hangar.model.common.Color;
public class EditChannelForm extends ChannelForm {
private final long id;
public EditChannelForm(String name, Color color, boolean nonReviewed, long id) {
super(name, color, nonReviewed);
this.id = id;
}
public long getId() {
return id;
}
@Override
public String toString() {
return "EditChannelForm{" +
"id=" + id +
"} " + super.toString();
}
}

View File

@ -9,6 +9,7 @@ import java.lang.annotation.Target;
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Deprecated(forRemoval = true)
public @interface ProjectPermission {
NamedPermission[] value() default {};
}

View File

@ -10,6 +10,7 @@ import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Deprecated(forRemoval = true)
public @interface UserLock {
@AliasFor("route")
Routes value() default Routes.SHOW_HOME;

View File

@ -12,6 +12,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ChannelService extends HangarService {
@ -24,27 +25,41 @@ public class ChannelService extends HangarService {
this.hangarProjectsDAO = hangarProjectsDAO.get();
}
public ProjectChannelTable createProjectChannel(String name, Color color, long projectId, boolean nonReviewed) {
private void validateChannel(String name, Color color, long projectId, boolean nonReviewed, List<ProjectChannelTable> existingChannels) {
if (!config.channels.isValidChannelName(name)) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "channel.modal.error.invalidName");
}
List<ProjectChannelTable> existingTables = projectChannelsDAO.getProjectChannels(projectId);
if (existingTables.size() >= config.projects.getMaxChannels()) {
if (existingChannels.size() >= config.projects.getMaxChannels()) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "channel.modal.error.maxChannels", config.projects.getMaxChannels());
}
if (existingTables.stream().anyMatch(ch -> ch.getColor() == color)) {
if (existingChannels.stream().anyMatch(ch -> ch.getColor() == color)) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "channel.modal.error.duplicateColor");
}
if (existingTables.stream().anyMatch(ch -> ch.getName().equalsIgnoreCase(name))) {
if (existingChannels.stream().anyMatch(ch -> ch.getName().equalsIgnoreCase(name))) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "channel.modal.error.duplicateName");
}
}
public ProjectChannelTable createProjectChannel(String name, Color color, long projectId, boolean nonReviewed) {
validateChannel(name, color, projectId, nonReviewed, projectChannelsDAO.getProjectChannels(projectId));
return projectChannelsDAO.insert(new ProjectChannelTable(name, color, projectId, nonReviewed));
}
public void editProjectChannel(long channelId, String name, Color color, long projectId, boolean nonReviewed) {
ProjectChannelTable projectChannelTable = getProjectChannel(channelId);
if (projectChannelTable == null) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
validateChannel(name, color, projectId, nonReviewed, projectChannelsDAO.getProjectChannels(projectId).stream().filter(ch -> ch.getId() != channelId).collect(Collectors.toList()));
projectChannelTable.setName(name);
projectChannelTable.setColor(color);
projectChannelTable.setNonReviewed(nonReviewed);
projectChannelsDAO.update(projectChannelTable);
}
public List<HangarChannel> getProjectChannels(long projectId) {
return hangarProjectsDAO.getHangarChannels(projectId);
}
@ -53,6 +68,10 @@ public class ChannelService extends HangarService {
return projectChannelsDAO.getProjectChannel(projectId, name, color);
}
public ProjectChannelTable getProjectChannel(long channelId) {
return projectChannelsDAO.getProjectChannel(channelId);
}
public ProjectChannelTable getProjectChannelForVersion(long versionId) {
return projectChannelsDAO.getProjectChannelForVersion(versionId);
}