feat(projects): optionally return max right when querying all projects
This change introduces an expand query parameter which, when provided, allows to return all projects with the max right the current user has on that project. This allows to show and hide appropriate buttons in the frontend. Resolves https://github.com/go-vikunja/vikunja/issues/334
This commit is contained in:
parent
3e9c41cfc6
commit
92b90013ab
|
|
@ -25,7 +25,7 @@
|
|||
/>
|
||||
<div
|
||||
class="color-bubble-handle-wrapper"
|
||||
:class="{'is-draggable': project.id > 0}"
|
||||
:class="{'is-draggable': project.id > 0 && project.maxRight > RIGHTS.READ}"
|
||||
>
|
||||
<ColorBubble
|
||||
v-if="project.hexColor !== ''"
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
<Icon icon="filter" />
|
||||
</span>
|
||||
<span
|
||||
v-if="project.id > 0"
|
||||
v-if="project.id > 0 && project.maxRight > RIGHTS.READ"
|
||||
class="icon menu-item-icon handle"
|
||||
:class="{'has-color-bubble': project.hexColor !== ''}"
|
||||
>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
<span class="project-menu-title">{{ getProjectTitle(project) }}</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="project.id > 0"
|
||||
v-if="project.id > 0 && project.maxRight > RIGHTS.READ"
|
||||
class="favorite"
|
||||
:class="{'is-favorite': project.isFavorite}"
|
||||
@click="projectStore.toggleProjectFavorite(project)"
|
||||
|
|
@ -56,6 +56,7 @@
|
|||
<Icon :icon="project.isFavorite ? 'star' : ['far', 'star']" />
|
||||
</BaseButton>
|
||||
<ProjectSettingsDropdown
|
||||
v-if="project.maxRight > RIGHTS.READ"
|
||||
class="menu-list-dropdown"
|
||||
:project="project"
|
||||
>
|
||||
|
|
@ -94,6 +95,7 @@ import ProjectSettingsDropdown from '@/components/project/ProjectSettingsDropdow
|
|||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import ColorBubble from '@/components/misc/ColorBubble.vue'
|
||||
import ProjectsNavigation from '@/components/home/ProjectsNavigation.vue'
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
const props = defineProps<{
|
||||
project: IProject,
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@
|
|||
{{ $t('menu.createProject') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-if="project.maxRight === RIGHTS.ADMIN"
|
||||
v-tooltip="isDefaultProject ? $t('menu.cantDeleteIsDefault') : ''"
|
||||
:to="{ name: 'project.settings.delete', params: { projectId: project.id } }"
|
||||
icon="trash-alt"
|
||||
|
|
@ -135,6 +136,7 @@ import {isSavedFilter} from '@/services/savedFilter'
|
|||
import {useConfigStore} from '@/stores/config'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
const props = defineProps<{
|
||||
project: IProject
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ export const useProjectStore = defineStore('project', () => {
|
|||
let page = 1
|
||||
try {
|
||||
do {
|
||||
const newProjects = await projectService.getAll({}, {is_archived: true}, page) as IProject[]
|
||||
const newProjects = await projectService.getAll({}, {is_archived: true, expand: 'rights'}, page) as IProject[]
|
||||
loadedProjects.push(...newProjects)
|
||||
page++
|
||||
} while (page <= projectService.totalPages)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
:title="$t('project.edit.header')"
|
||||
primary-icon=""
|
||||
:primary-label="$t('misc.save')"
|
||||
:tertiary="$t('misc.delete')"
|
||||
:tertiary="project.maxRight === RIGHTS.ADMIN ? $t('misc.delete') : undefined"
|
||||
@primary="save"
|
||||
@tertiary="$router.push({ name: 'project.settings.delete', params: { id: projectId } })"
|
||||
>
|
||||
|
|
@ -99,6 +99,7 @@ import {useProjectStore} from '@/stores/projects'
|
|||
import {useProject} from '@/stores/projects'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: IProject['id'],
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ type Project struct {
|
|||
|
||||
Views []*ProjectView `xorm:"-" json:"views"`
|
||||
|
||||
Expand ProjectExpandable `xorm:"-" json:"-" query:"expand"`
|
||||
MaxRight Right `xorm:"-" json:"max_right"`
|
||||
|
||||
// A timestamp when this project was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this project was last updated. You cannot change this value.
|
||||
|
|
@ -86,6 +89,10 @@ type Project struct {
|
|||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
type ProjectExpandable string
|
||||
|
||||
const ProjectExpandableRights = `rights`
|
||||
|
||||
type ProjectWithTasksAndBuckets struct {
|
||||
Project
|
||||
ChildProjects []*ProjectWithTasksAndBuckets `xorm:"-" json:"child_projects"`
|
||||
|
|
@ -161,6 +168,7 @@ var FavoritesPseudoProject = Project{
|
|||
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
||||
// @Param s query string false "Search projects by title."
|
||||
// @Param is_archived query bool false "If true, also returns all archived projects."
|
||||
// @Param expand query string false "If set to `rights`, Vikunja will return the max right the current user has on this project. You can currently only set this to `rights`."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.Project "The projects"
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
|
||||
|
|
@ -219,6 +227,17 @@ func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
|
|||
return
|
||||
}
|
||||
|
||||
if p.Expand == ProjectExpandableRights {
|
||||
err = addMaxRightToProjects(s, prs, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
for _, pr := range prs {
|
||||
pr.MaxRight = RightUnknown
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// Putting it all together
|
||||
|
||||
|
|
@ -719,6 +738,31 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
|
|||
return
|
||||
}
|
||||
|
||||
func addMaxRightToProjects(s *xorm.Session, projects []*Project, a web.Auth) (err error) {
|
||||
projectIDs := make([]int64, 0, len(projects))
|
||||
for _, project := range projects {
|
||||
if getSavedFilterIDFromProjectID(project.ID) > 0 {
|
||||
project.MaxRight = RightAdmin
|
||||
continue
|
||||
}
|
||||
projectIDs = append(projectIDs, project.ID)
|
||||
}
|
||||
|
||||
rights, err := checkRightsForProjects(s, a, projectIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
right, has := rights[project.ID]
|
||||
if has {
|
||||
project.MaxRight = right.MaxRight
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CheckIsArchived returns an ErrProjectIsArchived if the project or any of its parent projects is archived.
|
||||
func (p *Project) CheckIsArchived(s *xorm.Session) (err error) {
|
||||
if p.ParentProjectID > 0 {
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ package models
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/web"
|
||||
"xorm.io/builder"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
|
|
@ -105,9 +106,6 @@ func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
|||
(shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), int(shareAuth.Right), nil
|
||||
}
|
||||
|
||||
if p.isOwner(&user.User{ID: a.GetID()}) {
|
||||
return true, int(RightAdmin), nil
|
||||
}
|
||||
return p.checkRight(s, a, RightRead, RightWrite, RightAdmin)
|
||||
}
|
||||
|
||||
|
|
@ -211,66 +209,85 @@ func (p *Project) isOwner(u *user.User) bool {
|
|||
|
||||
// Checks n different rights for any given user
|
||||
func (p *Project) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
|
||||
projectRights, err := checkRightsForProjects(s, a, []int64{p.ID})
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
right, has := projectRights[p.ID]
|
||||
if !has {
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
var conds []builder.Cond
|
||||
for _, r := range rights {
|
||||
// User conditions
|
||||
// If the project was shared directly with the user and the user has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"ul.user_id": a.GetID()},
|
||||
builder.Eq{"ul.right": r},
|
||||
))
|
||||
|
||||
// Team rights
|
||||
// If the project was shared directly with the team and the team has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"tm2.user_id": a.GetID()},
|
||||
builder.Eq{"tl.right": r},
|
||||
))
|
||||
}
|
||||
|
||||
type allProjectRights struct {
|
||||
UserProject *ProjectUser `xorm:"extends"`
|
||||
TeamProject *TeamProject `xorm:"extends"`
|
||||
}
|
||||
|
||||
r := &allProjectRights{}
|
||||
var maxRight = 0
|
||||
exists, err := s.
|
||||
Select("p.*, ul.right, tl.right").
|
||||
Table("projects").
|
||||
Alias("p").
|
||||
// User stuff
|
||||
Join("LEFT", []string{"users_projects", "ul"}, "ul.project_id = p.id").
|
||||
// Team stuff
|
||||
Join("LEFT", []string{"team_projects", "tl"}, "p.id = tl.project_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
// The actual condition
|
||||
Where(builder.And(
|
||||
builder.Or(
|
||||
conds...,
|
||||
),
|
||||
builder.Eq{"p.id": p.ID},
|
||||
)).
|
||||
Get(r)
|
||||
|
||||
// If there's noting shared for this project, and it has a parent, go up the tree
|
||||
if !exists && p.ParentProjectID > 0 {
|
||||
parent, err := GetProjectSimpleByID(s, p.ParentProjectID)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
if r == right.MaxRight {
|
||||
return true, int(right.MaxRight), nil
|
||||
}
|
||||
|
||||
return parent.checkRight(s, a, rights...)
|
||||
}
|
||||
|
||||
// Figure out the max right and return it
|
||||
if int(r.UserProject.Right) > maxRight {
|
||||
maxRight = int(r.UserProject.Right)
|
||||
}
|
||||
if int(r.TeamProject.Right) > maxRight {
|
||||
maxRight = int(r.TeamProject.Right)
|
||||
}
|
||||
|
||||
return exists, maxRight, err
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
type projectRight struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
MaxRight Right
|
||||
}
|
||||
|
||||
func checkRightsForProjects(s *xorm.Session, a web.Auth, projectIDs []int64) (projectRightMap map[int64]*projectRight, err error) {
|
||||
projectRightMap = make(map[int64]*projectRight)
|
||||
whereIDIn := strings.Repeat("?,", len(projectIDs))[:len(projectIDs)*2-1]
|
||||
args := []interface{}{
|
||||
a.GetID(),
|
||||
a.GetID(),
|
||||
a.GetID(),
|
||||
int64(0),
|
||||
}
|
||||
for _, id := range projectIDs {
|
||||
args = append(args, id)
|
||||
}
|
||||
|
||||
err = s.SQL(`WITH RECURSIVE
|
||||
all_projects AS (SELECT p.id,
|
||||
p.parent_project_id,
|
||||
CASE
|
||||
WHEN p.owner_id = 1 THEN 2
|
||||
WHEN COALESCE(ul.right, 0) > COALESCE(tl.right, 0) THEN ul.right
|
||||
ELSE COALESCE(tl.right, 0)
|
||||
END AS initial_right
|
||||
FROM projects p
|
||||
LEFT JOIN team_projects tl ON tl.project_id = p.id
|
||||
LEFT JOIN team_members tm2 ON tm2.team_id = tl.team_id
|
||||
LEFT JOIN users_projects ul ON ul.project_id = p.id
|
||||
WHERE (tm2.user_id = ? OR ul.user_id = ? OR p.owner_id = ?)
|
||||
AND (p.parent_project_id IS NULL OR p.parent_project_id = ? OR
|
||||
((tm2.user_id IS NOT NULL OR ul.user_id IS NOT NULL) AND
|
||||
p.parent_project_id IS NOT NULL))
|
||||
AND p.id in (`+whereIDIn+`)
|
||||
GROUP BY p.id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT p.id, p.parent_project_id, ap.initial_right
|
||||
FROM projects p
|
||||
INNER JOIN all_projects ap ON p.parent_project_id = ap.id),
|
||||
|
||||
project_max_rights AS (SELECT id, MAX(initial_right) AS max_right
|
||||
FROM all_projects
|
||||
GROUP BY id),
|
||||
|
||||
inherited_rights AS (SELECT ap.id,
|
||||
CASE
|
||||
WHEN COALESCE(pmr.max_right, 0) > COALESCE(parent.max_right, 0)
|
||||
THEN COALESCE(pmr.max_right, 0)
|
||||
ELSE COALESCE(parent.max_right, 0)
|
||||
END AS inherited_right
|
||||
FROM all_projects ap
|
||||
LEFT JOIN project_max_rights pmr ON ap.id = pmr.id
|
||||
LEFT JOIN project_max_rights parent ON ap.parent_project_id = parent.id)
|
||||
|
||||
SELECT DISTINCT ap.id,
|
||||
ir.inherited_right AS max_right
|
||||
FROM all_projects ap
|
||||
LEFT JOIN all_projects np ON ap.parent_project_id = np.id
|
||||
LEFT JOIN inherited_rights ir ON ap.id = ir.id`, args...).Find(&projectRightMap)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,18 +16,20 @@
|
|||
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Right defines the rights users/teams can have for projects
|
||||
type Right int
|
||||
|
||||
// define unknown right
|
||||
const (
|
||||
RightUnknown = -1
|
||||
)
|
||||
|
||||
// Enumerate all the team rights
|
||||
const (
|
||||
// Can read projects in a
|
||||
RightRead Right = iota
|
||||
RightRead Right = iota - 1
|
||||
// Can write in a like projects and tasks. Cannot create new projects.
|
||||
RightWrite
|
||||
// Can manage a project, can do everything
|
||||
|
|
@ -41,3 +43,33 @@ func (r Right) isValid() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON marshals the enum as a quoted json string
|
||||
func (r Right) MarshalJSON() ([]byte, error) {
|
||||
if r == RightUnknown {
|
||||
return []byte(`null`), nil
|
||||
}
|
||||
return []byte(strconv.Itoa(int(r))), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals a quoted json string to the enum value
|
||||
func (r *Right) UnmarshalJSON(data []byte) error {
|
||||
var s int
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch s {
|
||||
case -1:
|
||||
*r = RightUnknown
|
||||
case 0:
|
||||
*r = RightRead
|
||||
case 1:
|
||||
*r = RightWrite
|
||||
case 2:
|
||||
*r = RightAdmin
|
||||
default:
|
||||
return fmt.Errorf("invalid Right %q", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue