project visibility stuff

This commit is contained in:
Jake Potrebic 2021-03-27 23:03:13 -07:00
parent 783cde2c87
commit 4101ce9fbf
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
28 changed files with 550 additions and 167 deletions

View File

@ -99,4 +99,8 @@
display: inline-block;
}
}
p:last-child {
margin-bottom: 0;
}
}

View File

@ -6,15 +6,16 @@
<v-card>
<v-card-title>{{ title }}</v-card-title>
<v-card-text>
<v-form ref="modalForm" v-model="validForm">
<v-form v-if="!noForm" ref="modalForm" v-model="validForm">
<slot />
</v-form>
<slot v-else />
</v-card-text>
<v-card-actions class="justify-end">
<v-btn text color="warning" @click.stop="close">{{ $t('general.close') }}</v-btn>
<slot name="other-btns" />
<v-btn color="success" :disabled="!validForm || submitDisabled" :loading="loading" @click.stop="submit0">
{{ submitLabel }}
<v-btn color="success" :disabled="(!noForm && !validForm) || submitDisabled" :loading="loading" @click.stop="submit0">
{{ submitLabel || $t('general.submit') }}
</v-btn>
</v-card-actions>
</v-card>
@ -35,8 +36,8 @@ export default class HangarModal extends HangarFormModal {
@Prop({ type: String as PropType<TranslateResult>, required: true })
title!: TranslateResult;
@Prop({ type: String as PropType<TranslateResult>, required: true })
submitLabel!: TranslateResult;
@Prop({ type: String as PropType<TranslateResult> })
submitLabel!: TranslateResult | null;
@Prop({ type: Function as PropType<() => Promise<void>>, required: true })
submit!: () => Promise<void>;
@ -44,12 +45,18 @@ export default class HangarModal extends HangarFormModal {
@Prop({ type: Boolean, default: false })
submitDisabled!: boolean;
@Prop({ type: Boolean, default: false })
noForm!: boolean;
$refs!: {
modalForm: any;
};
close() {
this.$refs.modalForm.reset();
if (!this.noForm) {
this.$refs.modalForm.reset();
}
this.dialog = false;
this.$emit('close');
}
@ -59,7 +66,9 @@ export default class HangarModal extends HangarFormModal {
this.submit()
.then(() => {
this.dialog = false;
this.$refs.modalForm.reset();
if (!this.noForm) {
this.$refs.modalForm.reset();
}
})
.finally(() => {
this.loading = false;

View File

@ -0,0 +1,98 @@
<template>
<HangarModal :title="$t('visibility.modal.title', [type])" :submit="submit" no-form :submit-disabled="disableSubmit">
<template #activator="{ on, attrs }">
<v-btn :small="smallBtn" v-bind="attrs" color="warning" class="mr-1" :class="activatorClass" v-on="on">
<v-icon :small="smallBtn" left>mdi-eye</v-icon>
{{ $t('visibility.modal.activatorBtn') }}
</v-btn>
</template>
<v-radio-group v-model="formVisibility" mandatory>
<v-radio v-for="vis in visibilities" :key="vis.name" :value="vis.name" :label="$t(vis.title)" />
</v-radio-group>
<v-form ref="reasonForm" v-model="validForm">
<!-- TODO this should be a markdown editor since the reason is rendered in markdown-->
<v-textarea
v-if="showTextarea"
v-model.trim="reason"
filled
hide-details
auto-grow
rows="2"
:rules="[$util.$vc.require()]"
:label="$t('visibility.modal.reason')"
/>
</v-form>
</HangarModal>
</template>
<script lang="ts">
import { Component, Prop, Watch } from 'nuxt-property-decorator';
import { IVisibility } from 'hangar-internal';
import { PropType } from 'vue';
import { HangarFormModal } from '~/components/mixins';
import { Visibility } from '~/types/enums';
import HangarModal from '~/components/modals/HangarModal.vue';
@Component({
components: { HangarModal },
})
export default class VisibilityChangerModal extends HangarFormModal {
@Prop({ type: String as PropType<Visibility>, required: true })
propVisibility!: Visibility;
@Prop({ type: String as PropType<'project' | 'version'>, required: true })
type!: 'project' | 'version';
@Prop({ type: String, required: true })
postUrl!: string;
@Prop({ type: Boolean, default: false })
smallBtn!: boolean;
visibilities: IVisibility[] = [];
formVisibility: Visibility = this.propVisibility;
reason: string = '';
$refs!: {
reasonForm: any;
};
submit(): Promise<void> {
return this.$api
.requestInternal(this.postUrl, true, 'post', {
visibility: this.formVisibility,
comment: this.currentIVis.showModal ? this.reason : null,
})
.then(() => {
this.reason = '';
this.$util.success(this.$t('visibility.modal.success', [this.type, this.$t(this.currentIVis?.title)]));
this.$nuxt.refresh();
})
.catch(this.$util.handleRequestError);
}
@Watch('currentVisibility')
onPropChange() {
this.formVisibility = this.propVisibility;
this.$refs.reasonForm.resetValidation();
}
get disableSubmit(): boolean {
return this.propVisibility === this.formVisibility || !this.validForm;
}
get showTextarea(): boolean {
return this.currentIVis?.showModal && this.propVisibility !== this.formVisibility;
}
get currentIVis(): IVisibility {
return this.visibilities.find((v) => v.name === this.formVisibility)!;
}
async fetch() {
this.visibilities = (await this.$api.requestInternal<IVisibility[]>('data/visibilities', false).catch<any>(this.$util.handlePageRequestError)) || [];
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -455,18 +455,28 @@ const msgs: LocaleMessageObject = {
notice: {
new:
'This project is new, and will not be shown to others until a version has been uploaded. If a version is not uploaded over a longer time the project will be deleted.',
needsChanges: 'This project requires changes: {0}',
needsChanges: 'This project requires changes',
needsApproval: 'You have sent the project for review',
softDelete: 'Project deleted by {0}',
},
name: {
new: 'New',
public: 'Public',
needsChanges: 'Needs Changes',
needsApproval: 'Needs Approval',
softDelete: 'Soft Delete',
},
changes: {
version: {
reviewed: 'due to approved reviews',
},
},
modal: {
activatorBtn: 'Visibility Actions',
title: "Change {0}'s visibility",
reason: 'Reason for change',
success: "You changed the {0}'s visibility to {1}",
},
},
author: {
watching: 'Watching',
@ -601,7 +611,6 @@ const msgs: LocaleMessageObject = {
msgUser: 'Message user',
msgProjectOwner: 'Message owner',
markResolved: 'Mark resolved',
visibilityActions: 'Visibility actions',
line1: '{0} reported {1} on {2}',
line2: 'Reason: {0}',
line3: 'Comment: {0}',

View File

@ -2,11 +2,18 @@
<div>
<template v-if="!isPublic">
<!-- todo alert for visibility stuff -->
<v-alert v-if="needsChanges" type="error">
<v-btn v-if="$perms.canEditPage" type="primary" :to="'/' + slug + '/manage/sendforapproval'">{{ $t('project.sendForApproval') }} </v-btn>
<strong>{{ $t('visibility.notice.' + project.visibility) }}</strong>
<br />
<Markdown :raw="project.lastVisibilityChangeComment || 'Unknown'" />
<v-alert v-if="needsChanges" type="error" text>
<v-row>
<v-col class="grow">
<strong>{{ $t('visibility.notice.' + project.visibility) }}</strong>
</v-col>
<v-col v-if="$perms.canEditPage" class="shrink">
<v-btn v-if="$perms.canEditPage" :loading="loading.approval" color="warning" @click.stop="sendForApproval">
{{ $t('project.sendForApproval') }}
</v-btn>
</v-col>
</v-row>
<Markdown :raw="project.lastVisibilityChangeComment || 'Unknown'" style="z-index: 10; position: relative" class="mt-2" />
</v-alert>
<v-alert v-else-if="isSoftDeleted" type="error">
{{ $t('visibility.notice.' + project.visibility, [project.lastVisibilityChangeUserName]) }}
@ -111,7 +118,7 @@ import { HangarProject } from 'hangar-internal';
import { NavigationGuardNext, Route } from 'vue-router';
import { TranslateResult } from 'vue-i18n';
import { Markdown } from '~/components/markdown';
import FlagModal from '~/components/modals/FlagModal.vue';
import FlagModal from '~/components/modals/projects/FlagModal.vue';
import { UserAvatar } from '~/components/users';
import { Visibility } from '~/types/enums';
import { HangarComponent } from '~/components/mixins';
@ -133,6 +140,9 @@ interface Tab {
})
export default class ProjectPage extends HangarComponent {
project!: HangarProject;
loading = {
approval: false,
};
head() {
return {
@ -212,6 +222,20 @@ export default class ProjectPage extends HangarComponent {
.catch((err) => this.$util.handleRequestError(err, 'project.error.watch'));
}
sendForApproval() {
this.loading.approval = true;
this.$api
.requestInternal(`projects/visibility/${this.project.id}/sendforapproval`, true, 'post')
.then(() => {
this.$util.success('SUCCESS');
this.$nuxt.refresh();
})
.catch(this.$util.handleRequestError)
.finally(() => {
this.loading.approval = false;
});
}
// Need to refresh the project if anything has changed. idk if this is the best way to do this
async beforeRouteUpdate(to: Route, _from: Route, next: NavigationGuardNext) {
this.project = await this.$api

View File

@ -4,7 +4,21 @@
<v-card class="settings-card">
<v-card-title class="sticky">
{{ $t('project.settings.title') }}
<v-btn class="flex-right" color="success" :loading="loading.save" :disabled="!validForm.settings" @click="save">
<VisibilityChangerModal
v-if="$perms.canSeeHidden"
type="project"
:prop-visibility="project.visibility"
activator-class="flex-right"
:post-url="`projects/visibility/${project.id}`"
/>
<v-btn
class="flex-right"
:class="{ 'ml-1': $perms.canSeeHidden }"
color="success"
:loading="loading.save"
:disabled="!validForm.settings"
@click="save"
>
<v-icon left>mdi-check</v-icon>
{{ $t('project.settings.save') }}
</v-btn>
@ -292,9 +306,10 @@ import { NamedPermission, ProjectCategory } from '~/types/enums';
import { RootState } from '~/store';
import { HangarProjectMixin } from '~/components/mixins';
import { MemberList } from '~/components/projects';
import VisibilityChangerModal from '~/components/modals/VisibilityChangerModal.vue';
@Component({
components: { MemberList },
components: { VisibilityChangerModal, MemberList },
})
@ProjectPermission(NamedPermission.EDIT_SUBJECT_SETTINGS)
export default class ProjectManagePage extends HangarProjectMixin {

View File

@ -2,7 +2,6 @@
<v-card>
<v-card-title>{{ $t('flagReview.title') }}</v-card-title>
<v-card-text>
<!-- TODO link to project -->
<v-list v-if="flags.length > 0">
<v-list-item v-for="flag in flags" :key="flag.id">
<v-list-item-avatar>
@ -17,6 +16,9 @@
$util.prettyDateTime(flag.createdAt),
])
}}
<v-btn small icon color="primary" :to="`/${flag.projectNamespace.owner}/${flag.projectNamespace.slug}`" nuxt target="_blank">
<v-icon small>mdi-open-in-new</v-icon>
</v-btn>
</v-list-item-title>
<v-list-item-subtitle>{{ $t('flagReview.line2', [$t(flag.reason)]) }}</v-list-item-subtitle>
<v-list-item-subtitle>{{ $t('flagReview.line3', [flag.comment]) }}</v-list-item-subtitle>
@ -30,22 +32,16 @@
<v-icon small left>mdi-reply</v-icon>
{{ $t('flagReview.msgProjectOwner') }}
</v-btn>
<v-menu offset-y>
<template #activator="{ on, attrs }">
<v-btn small v-bind="attrs" color="warning" class="mr-1" v-on="on">
<v-icon small left>mdi-eye</v-icon>
{{ $t('flagReview.visibilityActions') }}
</v-btn>
</template>
<v-list>
<v-list-item v-for="(v, index) in visibilities" :key="index" @click="visibility(flag, v)">
<v-list-item-title>{{ v }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn small color="success" :loading="loading[flag.id]" @click="resolve(flag)"
><v-icon small left>mdi-check</v-icon>{{ $t('flagReview.markResolved') }}</v-btn
>
<VisibilityChangerModal
:prop-visibility="flag.projectVisibility"
type="project"
:post-url="`projects/visibility/${flag.projectId}`"
small-btn
/>
<v-btn small color="success" :loading="loading[flag.id]" @click="resolve(flag)">
<v-icon small left>mdi-check</v-icon>
{{ $t('flagReview.markResolved') }}
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list>
@ -59,26 +55,18 @@ import { Component, Vue } from 'nuxt-property-decorator';
import { Flag } from 'hangar-internal';
import { Context } from '@nuxt/types';
import UserAvatar from '~/components/users/UserAvatar.vue';
import { NamedPermission, Visibility } from '~/types/enums';
import { NamedPermission } from '~/types/enums';
import { GlobalPermission } from '~/utils/perms';
import VisibilityChangerModal from '~/components/modals/VisibilityChangerModal.vue';
@Component({
components: { UserAvatar },
components: { VisibilityChangerModal, UserAvatar },
})
@GlobalPermission(NamedPermission.MOD_NOTES_AND_FLAGS)
export default class AdminFlagsPage extends Vue {
flags!: Flag[];
loading: { [key: number]: boolean } = {};
get visibilities(): Visibility[] {
return Object.keys(Visibility) as Visibility[];
}
// todo send to server
visibility(flag: Flag, visibility: Visibility) {
console.log('changing visibility of ', flag, 'to ', visibility);
}
resolve(flag: Flag) {
this.loading[flag.id] = true;
this.$api

View File

@ -1,4 +1,6 @@
declare module 'hangar-api' {
import { Visibility } from '~/types/enums';
interface Model {
createdAt: string;
}
@ -33,4 +35,8 @@ declare module 'hangar-api' {
name: String;
link: String;
}
interface Visible {
visibility: Visibility;
}
}

View File

@ -1,6 +1,6 @@
declare module 'hangar-api' {
import { Model, Named, TagColor } from 'hangar-api';
import { ProjectCategory, Visibility } from '~/types/enums';
import { Model, Named, TagColor, Visible } from 'hangar-api';
import { ProjectCategory } from '~/types/enums';
interface ProjectNamespace {
owner: string;
@ -48,11 +48,10 @@ declare module 'hangar-api' {
tags: PromotedVersionTag[];
}
interface ProjectCompact extends Model, Named {
interface ProjectCompact extends Model, Named, Visible {
namespace: ProjectNamespace;
stats: ProjectStats;
category: ProjectCategory;
visibility: Visibility;
}
interface Project extends ProjectCompact {

View File

@ -1,6 +1,6 @@
declare module 'hangar-api' {
import { Model, Named, ProjectNamespace, TagColor } from 'hangar-api';
import { Platform, ReviewState, Visibility } from '~/types/enums';
import { Model, Named, ProjectNamespace, TagColor, Visible } from 'hangar-api';
import { Platform, ReviewState } from '~/types/enums';
interface PluginDependency extends Named {
required: boolean;
@ -27,9 +27,8 @@ declare module 'hangar-api' {
pluginDependencies: Record<Platform, PluginDependency[]>;
}
interface Version extends Model, Named, DependencyVersion {
interface Version extends Model, Named, DependencyVersion, Visible {
platformDependencies: Record<Platform, string[]>;
visibility: Visibility;
description: string;
stats: VersionStats;
fileInfo: FileInfo;

View File

@ -1,6 +1,6 @@
declare module 'hangar-internal' {
import { Model, TagColor } from 'hangar-api';
import { Platform, ProjectCategory } from '~/types/enums';
import { Platform, ProjectCategory, Visibility } from '~/types/enums';
interface Table extends Model {
id: number;
@ -31,4 +31,11 @@ declare module 'hangar-internal' {
tagColor: TagColor;
possibleVersions: string[];
}
interface IVisibility {
name: Visibility;
showModal: boolean;
cssClass: string;
title: string;
}
}

View File

@ -10,12 +10,12 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.model.Announcement;
import io.papermc.hangar.model.api.project.ProjectLicense;
import io.papermc.hangar.model.common.Color;
import io.papermc.hangar.model.common.NamedPermission;
import io.papermc.hangar.model.common.Platform;
import io.papermc.hangar.model.common.projects.Category;
import io.papermc.hangar.model.common.projects.FlagReason;
import io.papermc.hangar.model.common.projects.Visibility;
import io.papermc.hangar.model.common.roles.OrganizationRole;
import io.papermc.hangar.model.common.roles.ProjectRole;
import io.papermc.hangar.security.annotations.Anyone;
@ -142,6 +142,20 @@ public class BackendDataController {
return ResponseEntity.ok(config.getLicences());
}
@GetMapping("/visibilities")
public ResponseEntity<ArrayNode> getVisibilities() {
ArrayNode arrayNode = mapper.createArrayNode();
for (Visibility value : Visibility.getValues()) {
ObjectNode objectNode = mapper.createObjectNode();
objectNode.put("name", value.getName())
.put("showModal", value.getShowModal())
.put("cssClass", value.getCssClass())
.put("title", value.getTitle());
arrayNode.add(objectNode);
}
return ResponseEntity.ok(arrayNode);
}
@GetMapping("/validations")
public ResponseEntity<ObjectNode> getValidations() {
ObjectNode validations = mapper.createObjectNode();

View File

@ -10,6 +10,7 @@ import io.papermc.hangar.model.internal.api.requests.EditMembersForm;
import io.papermc.hangar.model.internal.api.requests.StringContent;
import io.papermc.hangar.model.internal.api.requests.projects.NewProjectForm;
import io.papermc.hangar.model.internal.api.requests.projects.ProjectSettingsForm;
import io.papermc.hangar.model.internal.api.requests.projects.VisibilityChangeForm;
import io.papermc.hangar.model.internal.api.responses.PossibleProjectOwner;
import io.papermc.hangar.model.internal.projects.HangarProject;
import io.papermc.hangar.model.internal.projects.HangarProjectNote;
@ -22,6 +23,7 @@ import io.papermc.hangar.service.internal.projects.ProjectFactory;
import io.papermc.hangar.service.internal.projects.ProjectNoteService;
import io.papermc.hangar.service.internal.projects.ProjectService;
import io.papermc.hangar.service.internal.users.UserService;
import io.papermc.hangar.service.internal.visibility.ProjectVisibilityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -51,14 +53,16 @@ public class ProjectController extends HangarController {
private final UserService userService;
private final OrganizationService organizationService;
private final ProjectNoteService projectNoteService;
private final ProjectVisibilityService projectVisibilityService;
@Autowired
public ProjectController(ProjectFactory projectFactory, ProjectService projectService, UserService userService, OrganizationService organizationService, ProjectNoteService projectNoteService) {
public ProjectController(ProjectFactory projectFactory, ProjectService projectService, UserService userService, OrganizationService organizationService, ProjectNoteService projectNoteService, ProjectVisibilityService projectVisibilityService) {
this.projectFactory = projectFactory;
this.projectService = projectService;
this.userService = userService;
this.organizationService = organizationService;
this.projectNoteService = projectNoteService;
this.projectVisibilityService = projectVisibilityService;
}
@GetMapping("/validateName")
@ -156,4 +160,20 @@ public class ProjectController extends HangarController {
public void addProjectNote(@PathVariable long projectId, @RequestBody @Valid StringContent content) {
projectNoteService.addNote(projectId, content.getContent());
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(perms = NamedPermission.REVIEWER)
@PostMapping(path = "/visibility/{projectId}", consumes = MediaType.APPLICATION_JSON_VALUE)
public void changeProjectVisibility(@PathVariable long projectId, @Valid @RequestBody VisibilityChangeForm visibilityChangeForm) {
projectVisibilityService.changeVisibility(projectId, visibilityChangeForm.getVisibility(), visibilityChangeForm.getComment());
}
@Unlocked
@ResponseStatus(HttpStatus.OK)
@PermissionRequired(type = PermissionType.PROJECT, perms = NamedPermission.EDIT_PAGE, args = "{#projectId}")
@PostMapping("/visibility/{projectId}/sendforapproval")
public void sendProjectForApproval(@PathVariable long projectId) {
projectService.sendProjectForApproval(projectId);
}
}

View File

@ -2,12 +2,16 @@ package io.papermc.hangar.db.dao.internal.table;
import io.papermc.hangar.model.db.visibility.ProjectVersionVisibilityChangeTable;
import io.papermc.hangar.model.db.visibility.ProjectVisibilityChangeTable;
import org.jdbi.v3.sqlobject.config.KeyColumn;
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
import org.jdbi.v3.sqlobject.customizer.BindBean;
import org.jdbi.v3.sqlobject.customizer.Timestamped;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.springframework.stereotype.Repository;
import java.util.Map.Entry;
@Repository
@RegisterConstructorMapper(ProjectVisibilityChangeTable.class)
@RegisterConstructorMapper(ProjectVersionVisibilityChangeTable.class)
@ -27,6 +31,14 @@ public interface VisibilityDAO {
"WHERE pvc.project_id = sq.project_id")
void updateLatestProjectChange(long userId, long projectId);
@KeyColumn("name")
@SqlQuery("SELECT pvc.*, u.name " +
"FROM project_visibility_changes pvc " +
" LEFT JOIN users u ON pvc.created_by = u.id " +
"WHERE pvc.project_id = :projectId " +
"ORDER BY pvc.created_at DESC LIMIT 1")
Entry<String, ProjectVisibilityChangeTable> getLatestProjectVisibilityChange(long projectId);
// Versions
@Timestamped
@SqlUpdate("INSERT INTO project_version_visibility_changes (created_at, created_by, version_id, comment, resolved_at, resolved_by, visibility) " +
@ -40,4 +52,12 @@ public interface VisibilityDAO {
" (SELECT version_id FROM project_version_visibility_changes WHERE version_id = :versionId AND resolved_at IS NULL AND resolved_by IS NULL ORDER BY created_at LIMIT 1) as subquery " +
"WHERE project_version_visibility_changes.version_id = subquery.version_id")
void updateLatestVersionChange(long userId, long versionId);
@KeyColumn("name")
@SqlQuery("SELECT pvvc.*, u.name " +
"FROM project_version_visibility_changes pvvc " +
" LEFT JOIN users u ON pvvc.created_by = u.id " +
"WHERE pvvc.version_id = :versionId " +
"ORDER BY pvvc.created_at DESC LIMIT 1")
Entry<String, ProjectVersionVisibilityChangeTable> getLatestVersionVisibilityChange(long versionId);
}

View File

@ -26,7 +26,7 @@ public interface ProjectsDAO {
@GetGeneratedKeys
@SqlUpdate("UPDATE projects SET name = :name, slug = :slug, category = :category, keywords = :keywords, issues = :issues, source = :source, " +
"license_name = :licenseName, license_url = :licenseUrl, forum_sync = :forumSync, description = :description, visibility = :visibility, " +
"notes = :notes, support = :support, homepage = :homepage WHERE id = :id")
"support = :support, homepage = :homepage WHERE id = :id")
ProjectTable update(@BindBean ProjectTable project);
@SqlUpdate("DELETE FROM projects WHERE id = :id")

View File

@ -16,6 +16,10 @@ public class HangarApiException extends ResponseStatusException {
private final HttpHeaders httpHeaders;
private final Object[] args;
public HangarApiException() {
this(HttpStatus.BAD_REQUEST);
}
public HangarApiException(String reason) {
this(HttpStatus.BAD_REQUEST, reason);
}

View File

@ -4,24 +4,26 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
public enum Visibility {
PUBLIC("public", false, ""),
PUBLIC("public", false, "", "visibility.name.public"),
NEW("new", false, "project-new"),
NEW("new", false, "project-new", "visibility.name.new"),
NEEDSCHANGES("needsChanges", true, "striped project-needsChanges"),
NEEDSCHANGES("needsChanges", true, "striped project-needsChanges", "visibility.name.needsChanges"),
NEEDSAPPROVAL("needsApproval", false, "striped project-needsChanges"),
NEEDSAPPROVAL("needsApproval", false, "striped project-needsChanges", "visibility.name.needsApproval"),
SOFTDELETE("softDelete", true, "striped project-hidden");
SOFTDELETE("softDelete", true, "striped project-hidden", "visibility.name.softDelete");
private final String name;
private final boolean showModal;
private final String cssClass;
private final String title;
Visibility(String name, boolean showModal, String cssClass) {
Visibility(String name, boolean showModal, String cssClass, String title) {
this.name = name;
this.showModal = showModal;
this.cssClass = cssClass;
this.title = title;
}
public String getName() {
@ -36,6 +38,10 @@ public enum Visibility {
return cssClass;
}
public String getTitle() {
return title;
}
@Override
@JsonValue
public String toString() {

View File

@ -0,0 +1,35 @@
package io.papermc.hangar.model.internal.api.requests.projects;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.papermc.hangar.model.common.projects.Visibility;
import javax.validation.constraints.NotNull;
public class VisibilityChangeForm {
@NotNull
private final Visibility visibility;
private final String comment;
@JsonCreator
public VisibilityChangeForm(Visibility visibility, String comment) {
this.visibility = visibility;
this.comment = comment;
}
public Visibility getVisibility() {
return visibility;
}
public String getComment() {
return comment;
}
@Override
public String toString() {
return "VisibilityChangeForm{" +
"visibility=" + visibility +
", comment='" + comment + '\'' +
'}';
}
}

View File

@ -1,97 +0,0 @@
package io.papermc.hangar.service;
import io.papermc.hangar.db.customtypes.LoggedActionType;
import io.papermc.hangar.db.customtypes.LoggedActionType.ProjectContext;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.table.VisibilityDAO;
import io.papermc.hangar.db.dao.internal.table.projects.ProjectsDAO;
import io.papermc.hangar.db.dao.internal.table.versions.ProjectVersionsDAO;
import io.papermc.hangar.model.ModelVisible;
import io.papermc.hangar.model.common.Permission;
import io.papermc.hangar.model.common.projects.Visibility;
import io.papermc.hangar.model.db.ProjectIdentified;
import io.papermc.hangar.model.db.Table;
import io.papermc.hangar.model.db.projects.ProjectTable;
import io.papermc.hangar.model.db.versions.ProjectVersionTable;
import io.papermc.hangar.model.db.visibility.ProjectVersionVisibilityChangeTable;
import io.papermc.hangar.model.db.visibility.ProjectVisibilityChangeTable;
import io.papermc.hangar.model.db.visibility.VisibilityChangeTable;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
public abstract class VisibilityService<M extends Table & ModelVisible & ProjectIdentified, VT extends VisibilityChangeTable> extends HangarService {
@Autowired
private PermissionService permissionService;
private final Consumer<VT> dbInsertion;
private final BiConsumer<Long, Long> lastUpdater;
private final VisibilityChangeTableConstructor<VT> changeTableConstructor;
private final Function<M, M> modelUpdater;
protected VisibilityService(Consumer<VT> dbInsertion, BiConsumer<Long, Long> lastUpdater, VisibilityChangeTableConstructor<VT> changeTableConstructor, Function<M, M> modelUpdater) {
this.dbInsertion = dbInsertion;
this.lastUpdater = lastUpdater;
this.changeTableConstructor = changeTableConstructor;
this.modelUpdater = modelUpdater;
}
public M changeVisibility(M model, Visibility newVisibility, String comment) {
if (model.getVisibility() == newVisibility) return model;
lastUpdater.accept(getHangarPrincipal().getUserId(), model.getId());
dbInsertion.accept(changeTableConstructor.create(getHangarPrincipal().getUserId(), comment, newVisibility, model.getId()));
model.setVisibility(newVisibility);
return modelUpdater.apply(model);
}
public final M checkVisibility(@Nullable M model) {
if (model == null) {
return null;
}
Permission perms = permissionService.getProjectPermissions(getHangarUserId(), model.getProjectId());
if (!perms.has(Permission.SeeHidden) && !perms.has(Permission.IsProjectMember) && model.getVisibility() != Visibility.PUBLIC) {
return null;
}
return model;
}
@FunctionalInterface
private interface VisibilityChangeTableConstructor<T extends VisibilityChangeTable> {
T create(long createdBy, String comment, Visibility visibility, long subjectId);
}
@Service
public static class ProjectVisibilityService extends VisibilityService<ProjectTable, ProjectVisibilityChangeTable> {
@Autowired
public ProjectVisibilityService(HangarDao<VisibilityDAO> visibilityDAO, HangarDao<ProjectsDAO> projectsDAO) {
super(visibilityDAO.get()::insert, visibilityDAO.get()::updateLatestProjectChange, ProjectVisibilityChangeTable::new, projectsDAO.get()::update);
}
@Override
public ProjectTable changeVisibility(ProjectTable model, Visibility newVisibility, String comment) {
Visibility oldVis = model.getVisibility();
// TODO add logging for visibility for versions and move this to the abstract class
userActionLogService.project(LoggedActionType.PROJECT_VISIBILITY_CHANGE.with(ProjectContext.of(model.getId())), newVisibility.getName(), oldVis.getName());
return super.changeVisibility(model, newVisibility, comment);
}
}
@Service
public static class ProjectVersionVisibilityService extends VisibilityService<ProjectVersionTable, ProjectVersionVisibilityChangeTable> {
@Autowired
public ProjectVersionVisibilityService(HangarDao<VisibilityDAO> visibilityDAO, HangarDao<ProjectVersionsDAO> projectVersionDAO) {
super(visibilityDAO.get()::insert, visibilityDAO.get()::updateLatestVersionChange, ProjectVersionVisibilityChangeTable::new, projectVersionDAO.get()::update);
}
}
}

View File

@ -11,6 +11,7 @@ import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.exceptions.MultiHangarApiException;
import io.papermc.hangar.model.api.project.Project;
import io.papermc.hangar.model.common.Permission;
import io.papermc.hangar.model.common.projects.Visibility;
import io.papermc.hangar.model.common.roles.ProjectRole;
import io.papermc.hangar.model.db.OrganizationTable;
import io.papermc.hangar.model.db.UserTable;
@ -25,12 +26,12 @@ import io.papermc.hangar.model.internal.projects.HangarProject.HangarProjectInfo
import io.papermc.hangar.model.internal.projects.HangarProjectPage;
import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.PermissionService;
import io.papermc.hangar.service.VisibilityService.ProjectVisibilityService;
import io.papermc.hangar.service.internal.organizations.OrganizationService;
import io.papermc.hangar.service.internal.roles.MemberService.ProjectMemberService;
import io.papermc.hangar.service.internal.roles.RoleService.ProjectRoleService;
import io.papermc.hangar.service.internal.uploads.ProjectFiles;
import io.papermc.hangar.service.internal.users.NotificationService;
import io.papermc.hangar.service.internal.visibility.ProjectVisibilityService;
import io.papermc.hangar.util.FileUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
@ -116,10 +117,18 @@ public class ProjectService extends HangarService {
Pair<Long, Project> project = hangarProjectsDAO.getProject(author, slug, getHangarUserId());
ProjectOwner projectOwner = getProjectOwner(author);
var members = hangarProjectsDAO.getProjectMembers(project.getLeft(), getHangarUserId(), permissionService.getProjectPermissions(getHangarUserId(), project.getLeft()).has(Permission.EditProjectSettings));
// TODO only include visibility change if not public (and if so, only include the user and comment)
String lastVisibilityChangeComment = "";
String lastVisibilityChangeUserName = "";
if (project.getRight().getVisibility() == Visibility.NEEDSCHANGES || project.getRight().getVisibility() == Visibility.SOFTDELETE) {
var projectVisibilityChangeTable = projectVisibilityService.getLastVisibilityChange(project.getLeft());
lastVisibilityChangeComment = projectVisibilityChangeTable.getValue().getComment();
if (project.getRight().getVisibility() == Visibility.SOFTDELETE) {
lastVisibilityChangeUserName = projectVisibilityChangeTable.getKey();
}
}
HangarProjectInfo info = hangarProjectsDAO.getHangarProjectInfo(project.getLeft());
Map<Long, HangarProjectPage> pages = projectPageService.getProjectPages(project.getLeft());
return new HangarProject(project.getRight(), project.getLeft(), projectOwner, members, "", "", info, pages.values());
return new HangarProject(project.getRight(), project.getLeft(), projectOwner, members, lastVisibilityChangeComment, lastVisibilityChangeUserName, info, pages.values());
}
public void saveSettings(String author, String slug, ProjectSettingsForm settingsForm) {
@ -233,6 +242,17 @@ public class ProjectService extends HangarService {
return projectsDAO.getProjectWatchers(projectId);
}
public void sendProjectForApproval(long projectId) {
ProjectTable projectTable = getProjectTable(projectId);
if (projectTable == null) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
if (projectTable.getVisibility() != Visibility.NEEDSCHANGES) {
throw new HangarApiException();
}
projectVisibilityService.changeVisibility(projectTable, Visibility.NEEDSAPPROVAL, "");
}
@Nullable
private <T> ProjectTable getProjectTable(@Nullable T identifier, @NotNull Function<T, ProjectTable> projectTableFunction) {
if (identifier == null) {

View File

@ -17,8 +17,8 @@ import io.papermc.hangar.model.db.versions.reviews.ProjectVersionReviewTable;
import io.papermc.hangar.model.internal.api.requests.versions.ReviewMessage;
import io.papermc.hangar.model.internal.versions.HangarReview;
import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.VisibilityService.ProjectVersionVisibilityService;
import io.papermc.hangar.service.internal.users.NotificationService;
import io.papermc.hangar.service.internal.visibility.ProjectVersionVisibilityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

View File

@ -22,7 +22,6 @@ import io.papermc.hangar.model.db.versions.ProjectVersionTable;
import io.papermc.hangar.model.db.versions.ProjectVersionTagTable;
import io.papermc.hangar.model.internal.versions.PendingVersion;
import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.VisibilityService.ProjectVisibilityService;
import io.papermc.hangar.service.api.UsersApiService;
import io.papermc.hangar.service.internal.projects.ChannelService;
import io.papermc.hangar.service.internal.projects.PlatformService;
@ -30,6 +29,7 @@ import io.papermc.hangar.service.internal.projects.ProjectService;
import io.papermc.hangar.service.internal.uploads.ProjectFiles;
import io.papermc.hangar.service.internal.users.NotificationService;
import io.papermc.hangar.service.internal.versions.plugindata.PluginFileWithData;
import io.papermc.hangar.service.internal.visibility.ProjectVisibilityService;
import io.papermc.hangar.util.CryptoUtils;
import io.papermc.hangar.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

View File

@ -9,7 +9,7 @@ import io.papermc.hangar.model.common.Platform;
import io.papermc.hangar.model.db.versions.ProjectVersionTable;
import io.papermc.hangar.model.internal.versions.HangarVersion;
import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.VisibilityService.ProjectVersionVisibilityService;
import io.papermc.hangar.service.internal.visibility.ProjectVersionVisibilityService;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;

View File

@ -0,0 +1,51 @@
package io.papermc.hangar.service.internal.visibility;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.table.VisibilityDAO;
import io.papermc.hangar.db.dao.internal.table.versions.ProjectVersionsDAO;
import io.papermc.hangar.model.db.versions.ProjectVersionTable;
import io.papermc.hangar.model.db.visibility.ProjectVersionVisibilityChangeTable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map.Entry;
@Service
public class ProjectVersionVisibilityService extends VisibilityService<ProjectVersionTable, ProjectVersionVisibilityChangeTable> {
private final ProjectVersionsDAO projectVersionsDAO;
private final VisibilityDAO visibilityDAO;
@Autowired
public ProjectVersionVisibilityService(HangarDao<VisibilityDAO> visibilityDAO, HangarDao<ProjectVersionsDAO> projectVersionDAO) {
super(ProjectVersionVisibilityChangeTable::new);
this.visibilityDAO = visibilityDAO.get();
this.projectVersionsDAO = projectVersionDAO.get();
}
@Override
void updateLastVisChange(long currentUserId, long modelId) {
visibilityDAO.updateLatestVersionChange(currentUserId, modelId);
}
@Override
ProjectVersionTable getModel(long id) {
return null;
}
@Override
ProjectVersionTable updateModel(ProjectVersionTable model) {
return projectVersionsDAO.update(model);
}
@Override
void insertNewVisibilityEntry(ProjectVersionVisibilityChangeTable visibilityTable) {
visibilityDAO.insert(visibilityTable);
}
@Override
public Entry<String, ProjectVersionVisibilityChangeTable> getLastVisibilityChange(long principalId) {
return visibilityDAO.getLatestVersionVisibilityChange(principalId);
}
}

View File

@ -0,0 +1,74 @@
package io.papermc.hangar.service.internal.visibility;
import io.papermc.hangar.db.customtypes.LoggedActionType;
import io.papermc.hangar.db.customtypes.LoggedActionType.ProjectContext;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.internal.projects.HangarProjectsDAO;
import io.papermc.hangar.db.dao.internal.table.VisibilityDAO;
import io.papermc.hangar.db.dao.internal.table.projects.ProjectsDAO;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.common.projects.Visibility;
import io.papermc.hangar.model.db.projects.ProjectTable;
import io.papermc.hangar.model.db.visibility.ProjectVisibilityChangeTable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.Map.Entry;
@Service
public class ProjectVisibilityService extends VisibilityService<ProjectTable, ProjectVisibilityChangeTable> {
private final ProjectsDAO projectsDAO;
private final VisibilityDAO visibilityDAO;
private final HangarProjectsDAO hangarProjectsDAO;
@Autowired
public ProjectVisibilityService(HangarDao<VisibilityDAO> visibilityDAO, HangarDao<ProjectsDAO> projectsDAO, HangarDao<HangarProjectsDAO> hangarProjectsDAO) {
super(ProjectVisibilityChangeTable::new);
this.projectsDAO = projectsDAO.get();
this.visibilityDAO = visibilityDAO.get();
this.hangarProjectsDAO = hangarProjectsDAO.get();
}
@Override
public ProjectTable changeVisibility(ProjectTable model, Visibility newVisibility, String comment) {
if (model == null) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
Visibility oldVis = model.getVisibility();
// TODO add logging for visibility for versions and move this to the abstract class
userActionLogService.project(LoggedActionType.PROJECT_VISIBILITY_CHANGE.with(ProjectContext.of(model.getId())), newVisibility.getName(), oldVis.getName());
return super.changeVisibility(model, newVisibility, comment);
}
@Override
void updateLastVisChange(long currentUserId, long modelId) {
visibilityDAO.updateLatestProjectChange(currentUserId, modelId);
}
@Override
ProjectTable getModel(long id) {
return projectsDAO.getById(id);
}
@Override
ProjectTable updateModel(ProjectTable model) {
return projectsDAO.update(model);
}
@Override
void insertNewVisibilityEntry(ProjectVisibilityChangeTable visibilityTable) {
visibilityDAO.insert(visibilityTable);
}
@Override
protected void postUpdate() {
hangarProjectsDAO.refreshHomeProjects();
}
@Override
public Entry<String, ProjectVisibilityChangeTable> getLastVisibilityChange(long principalId) {
return visibilityDAO.getLatestProjectVisibilityChange(principalId);
}
}

View File

@ -0,0 +1,78 @@
package io.papermc.hangar.service.internal.visibility;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.ModelVisible;
import io.papermc.hangar.model.common.Permission;
import io.papermc.hangar.model.common.projects.Visibility;
import io.papermc.hangar.model.db.ProjectIdentified;
import io.papermc.hangar.model.db.Table;
import io.papermc.hangar.model.db.visibility.VisibilityChangeTable;
import io.papermc.hangar.service.HangarService;
import io.papermc.hangar.service.PermissionService;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import java.util.Map.Entry;
public abstract class VisibilityService<M extends Table & ModelVisible & ProjectIdentified, VT extends VisibilityChangeTable> extends HangarService {
@Autowired
private PermissionService permissionService;
private final VisibilityChangeTableConstructor<VT> changeTableConstructor;
protected VisibilityService(VisibilityChangeTableConstructor<VT> changeTableConstructor) {
this.changeTableConstructor = changeTableConstructor;
}
public M changeVisibility(long modelId, Visibility newVisibility, String comment) {
return changeVisibility(getModel(modelId), newVisibility, comment);
}
public M changeVisibility(@Nullable M model, Visibility newVisibility, String comment) {
if (model == null) {
throw new HangarApiException(HttpStatus.NOT_FOUND);
}
if (model.getVisibility() == newVisibility) return model;
updateLastVisChange(getHangarPrincipal().getUserId(), model.getId());
insertNewVisibilityEntry(changeTableConstructor.create(getHangarPrincipal().getUserId(), comment == null ? "" : comment, newVisibility, model.getId()));
model.setVisibility(newVisibility);
model = updateModel(model);
postUpdate();
return model;
}
public final M checkVisibility(@Nullable M model) {
if (model == null) {
return null;
}
Permission perms = permissionService.getProjectPermissions(getHangarUserId(), model.getProjectId());
if (!perms.has(Permission.SeeHidden) && !perms.has(Permission.IsProjectMember) && model.getVisibility() != Visibility.PUBLIC) {
return null;
}
return model;
}
abstract void updateLastVisChange(long currentUserId, long modelId);
abstract M getModel(long id);
abstract M updateModel(M model);
abstract void insertNewVisibilityEntry(VT visibilityTable);
protected void postUpdate() { }
abstract public Entry<String, VT> getLastVisibilityChange(long principalId);
@FunctionalInterface
interface VisibilityChangeTableConstructor<T extends VisibilityChangeTable> {
T create(long createdBy, String comment, Visibility visibility, long subjectId);
}
}

View File

@ -16,8 +16,8 @@ import io.papermc.hangar.modelold.viewhelpers.ProjectData;
import io.papermc.hangar.modelold.viewhelpers.ReviewQueueEntry;
import io.papermc.hangar.modelold.viewhelpers.UserData;
import io.papermc.hangar.modelold.viewhelpers.VersionData;
import io.papermc.hangar.service.VisibilityService.ProjectVersionVisibilityService;
import io.papermc.hangar.service.internal.versions.VersionDependencyService;
import io.papermc.hangar.service.internal.visibility.ProjectVersionVisibilityService;
import io.papermc.hangar.serviceold.project.ChannelService;
import io.papermc.hangar.serviceold.project.ProjectService;
import io.papermc.hangar.util.RequestUtil;