refac: stale test entries, failing web tests, Postgre backup bug, Hardcoded MySQL table count, Stale Postgre connection in test, own-scope visibility filter, GetAll ignoring its error, JSON tags in use-case types, 31 copies of DESKTOP_QUERY, ProfileView endpoint paths
This commit is contained in:
1 parent
efecdee200
commit
5d6f378fce
87 files changed
+696
-373
No files matched your search
@@ -139,7 +139,12 @@ func (h *LoginHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *LoginHandler) RegistrationStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
body, _ := json.Marshal(map[string]bool{"open": h.CreateUseCase.IsRegistrationOpen()})
|
||||
open, err := h.CreateUseCase.IsRegistrationOpen()
|
||||
if err != nil {
|
||||
http.Error(w, apperr.RegistrationFailedErrorMessage, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
body, _ := json.Marshal(map[string]bool{"open": open})
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package auth
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/claims"
|
||||
"RenewCMS/api/pkg/payload"
|
||||
"RenewCMS/api/pkg/publicuser"
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/role"
|
||||
@@ -33,9 +34,9 @@ type meAuthor struct {
|
||||
}
|
||||
|
||||
type meAccess struct {
|
||||
Role useCases.RoleRef `json:"role"`
|
||||
Permissions []role.Grant `json:"permissions"`
|
||||
RoleAssignedBy *useCases.RoleAssigner `json:"role_assigned_by"`
|
||||
Role payload.RoleRef `json:"role"`
|
||||
Permissions []role.Grant `json:"permissions"`
|
||||
RoleAssignedBy *payload.RoleAssigner `json:"role_assigned_by"`
|
||||
}
|
||||
|
||||
type meResponse struct {
|
||||
@@ -46,9 +47,9 @@ type meResponse struct {
|
||||
|
||||
func toMeAccess(access useCases.Access) meAccess {
|
||||
return meAccess{
|
||||
Role: useCases.NewRoleRef(access.Role),
|
||||
Role: payload.RoleRefOf(useCases.NewRoleRef(access.Role)),
|
||||
Permissions: access.Permissions,
|
||||
RoleAssignedBy: access.RoleAssignedBy,
|
||||
RoleAssignedBy: payload.RoleAssignerPtrOf(access.RoleAssignedBy),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package ownership
|
||||
import (
|
||||
"RenewCMS/api/pkg/access"
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/payload"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
@@ -45,7 +46,7 @@ func (h *Handler) Transfer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
body, err := json.Marshal(transferred)
|
||||
body, err := json.Marshal(payload.OwnershipTransferOf(transferred))
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ package role
|
||||
import (
|
||||
"RenewCMS/api/pkg/access"
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/payload"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
domain "RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
@@ -152,7 +153,7 @@ func (h *Handler) List(w http.ResponseWriter, _ *http.Request) {
|
||||
writeRoleError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, listed)
|
||||
writeJSON(w, http.StatusOK, payload.RoleViewsOf(listed))
|
||||
}
|
||||
|
||||
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -165,7 +166,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
writeRoleError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, found)
|
||||
writeJSON(w, http.StatusOK, payload.RoleViewOf(found))
|
||||
}
|
||||
|
||||
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -180,7 +181,7 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
writeRoleError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, created)
|
||||
writeJSON(w, http.StatusCreated, payload.RoleViewOf(created))
|
||||
}
|
||||
|
||||
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -199,7 +200,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
writeRoleError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, updated)
|
||||
writeJSON(w, http.StatusOK, payload.RoleViewOf(updated))
|
||||
}
|
||||
|
||||
func (h *Handler) Reset(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -216,7 +217,7 @@ func (h *Handler) Reset(w http.ResponseWriter, r *http.Request) {
|
||||
writeRoleError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, restored)
|
||||
writeJSON(w, http.StatusOK, payload.RoleViewOf(restored))
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"RenewCMS/api/pkg/access"
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/claims"
|
||||
"RenewCMS/api/pkg/payload"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/domain/user"
|
||||
@@ -30,34 +31,34 @@ type CreateInviteRequest struct {
|
||||
}
|
||||
|
||||
type inviteResponse struct {
|
||||
ID uint32 `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Email string `json:"email"`
|
||||
Status string `json:"status"`
|
||||
Role useCases.RoleRef `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
ID uint32 `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Email string `json:"email"`
|
||||
Status string `json:"status"`
|
||||
Role payload.RoleRef `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type inviteListItem struct {
|
||||
ID uint32 `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Email string `json:"email"`
|
||||
Role *useCases.RoleRef `json:"role"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ID uint32 `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Email string `json:"email"`
|
||||
Role *payload.RoleRef `json:"role"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type userListItem struct {
|
||||
ID uint32 `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
Role *useCases.RoleRef `json:"role"`
|
||||
RoleAssignedBy *useCases.RoleAssigner `json:"role_assigned_by"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ID uint32 `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
Role *payload.RoleRef `json:"role"`
|
||||
RoleAssignedBy *payload.RoleAssigner `json:"role_assigned_by"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func NewInviteHandler(createInvite *useCases.CreateInviteUseCase, cancelInvite *useCases.CancelInviteUseCase, renewInvite *useCases.RenewInviteUseCase, listUsersAndInvites *useCases.ListUsersAndInvitesUseCase) *InviteHandler {
|
||||
@@ -107,7 +108,7 @@ func writeInviteResponse(w http.ResponseWriter, status int, entry useCases.Invit
|
||||
Kind: useCases.EntryKindInvite,
|
||||
Email: entry.Email,
|
||||
Status: string(entry.StatusAt(time.Now())),
|
||||
Role: entry.Role,
|
||||
Role: payload.RoleRefOf(entry.Role),
|
||||
CreatedAt: entry.CreatedAt,
|
||||
ExpiresAt: entry.ExpiresAt,
|
||||
})
|
||||
@@ -212,7 +213,7 @@ func (h *InviteHandler) ListUsers(w http.ResponseWriter, _ *http.Request) {
|
||||
ID: entry.ID,
|
||||
Kind: entry.Kind,
|
||||
Email: entry.Email,
|
||||
Role: entry.Role,
|
||||
Role: payload.RoleRefPtrOf(entry.Role),
|
||||
Status: entry.Status,
|
||||
CreatedAt: entry.CreatedAt,
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ package user
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/payload"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
@@ -16,8 +17,8 @@ func userListItemOf(entry useCases.DirectoryEntry) userListItem {
|
||||
Username: entry.Username,
|
||||
DisplayName: entry.DisplayName,
|
||||
Email: entry.Email,
|
||||
Role: entry.Role,
|
||||
RoleAssignedBy: entry.RoleAssignedBy,
|
||||
Role: payload.RoleRefPtrOf(entry.Role),
|
||||
RoleAssignedBy: payload.RoleAssignerPtrOf(entry.RoleAssignedBy),
|
||||
Status: entry.Status,
|
||||
CreatedAt: entry.CreatedAt,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package payload
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RoleRef struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
BuiltIn bool `json:"built_in"`
|
||||
Locked bool `json:"locked"`
|
||||
}
|
||||
|
||||
type RoleAssigner struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type RoleEditor struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type RoleView struct {
|
||||
role.Role
|
||||
Members int `json:"members"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
UpdatedBy *RoleEditor `json:"updated_by"`
|
||||
}
|
||||
|
||||
type OwnershipSuccessor struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type OwnershipPredecessor struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Role RoleRef `json:"role"`
|
||||
}
|
||||
|
||||
type OwnershipTransfer struct {
|
||||
Owner OwnershipSuccessor `json:"owner"`
|
||||
FormerOwner OwnershipPredecessor `json:"former_owner"`
|
||||
}
|
||||
|
||||
func RoleRefOf(ref useCases.RoleRef) RoleRef {
|
||||
return RoleRef(ref)
|
||||
}
|
||||
|
||||
func RoleRefPtrOf(ref *useCases.RoleRef) *RoleRef {
|
||||
if ref == nil {
|
||||
return nil
|
||||
}
|
||||
converted := RoleRefOf(*ref)
|
||||
return &converted
|
||||
}
|
||||
|
||||
func RoleAssignerPtrOf(assigner *useCases.RoleAssigner) *RoleAssigner {
|
||||
if assigner == nil {
|
||||
return nil
|
||||
}
|
||||
converted := RoleAssigner(*assigner)
|
||||
return &converted
|
||||
}
|
||||
|
||||
func RoleViewOf(view useCases.RoleView) RoleView {
|
||||
converted := RoleView{Role: view.Role, Members: view.Members, UpdatedAt: view.UpdatedAt}
|
||||
if view.UpdatedBy != nil {
|
||||
editor := RoleEditor(*view.UpdatedBy)
|
||||
converted.UpdatedBy = &editor
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func RoleViewsOf(views []useCases.RoleView) []RoleView {
|
||||
converted := make([]RoleView, 0, len(views))
|
||||
for _, view := range views {
|
||||
converted = append(converted, RoleViewOf(view))
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func OwnershipTransferOf(transfer useCases.OwnershipTransfer) OwnershipTransfer {
|
||||
return OwnershipTransfer{
|
||||
Owner: OwnershipSuccessor(transfer.Owner),
|
||||
FormerOwner: OwnershipPredecessor{ID: transfer.FormerOwner.ID, Name: transfer.FormerOwner.Name, Role: RoleRefOf(transfer.FormerOwner.Role)},
|
||||
}
|
||||
}
|
||||
@@ -30,14 +30,15 @@ func DefaultSort() Sort {
|
||||
}
|
||||
|
||||
type Filters struct {
|
||||
IsOnline *bool
|
||||
SiteID *uint32
|
||||
Page int
|
||||
Size int
|
||||
Q string
|
||||
Status string
|
||||
Sort Sort
|
||||
Unpaged bool
|
||||
IsOnline *bool
|
||||
SiteID *uint32
|
||||
VisibleTo *uint32
|
||||
Page int
|
||||
Size int
|
||||
Q string
|
||||
Status string
|
||||
Sort Sort
|
||||
Unpaged bool
|
||||
}
|
||||
|
||||
type Nullable[T any] struct {
|
||||
@@ -89,9 +90,10 @@ type Repository interface {
|
||||
FindByFilters(filters Filters) ([]Article, error)
|
||||
FindPage(filters Filters) (PageResult, error)
|
||||
CountByVisibility(onlineOnly bool) (Counts, error)
|
||||
CountVisibleTo(userID uint32) (Counts, error)
|
||||
Get(id uint32) (Article, error)
|
||||
GetByName(name string) (Article, error)
|
||||
GetAll() []Article
|
||||
GetAll() ([]Article, error)
|
||||
Create(post Article) (Article, error)
|
||||
UpdateBody(id uint32, body string) (Article, error)
|
||||
Update(id uint32, update Update) (Article, error)
|
||||
|
||||
@@ -48,6 +48,7 @@ func DefaultSort() Sort {
|
||||
|
||||
type Filters struct {
|
||||
ContentTypeID uint32
|
||||
CreatedBy *uint32
|
||||
Page int
|
||||
Size int
|
||||
Sort Sort
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
type Repository interface {
|
||||
Create(invite Invite) (Invite, error)
|
||||
GetAll() []Invite
|
||||
GetAll() ([]Invite, error)
|
||||
Delete(id uint32) error
|
||||
Renew(id uint32, hashedCode string, expiresAt time.Time, createdAt time.Time) (Invite, error)
|
||||
AcceptWithUser(inviteID uint32, acceptedAt time.Time, newUser user.User) (user.User, error)
|
||||
|
||||
@@ -6,7 +6,7 @@ type Repository interface {
|
||||
Get(id uint32) (User, error)
|
||||
GetByUsername(username string) (User, error)
|
||||
GetByEmail(email string) (User, error)
|
||||
GetAll() []User
|
||||
GetAll() ([]User, error)
|
||||
FindByIDs(ids []uint32) ([]User, error)
|
||||
Create(user User) (User, error)
|
||||
Delete(id uint32) error
|
||||
|
||||
@@ -48,6 +48,8 @@ var postgresNumericTypes = map[string]bool{
|
||||
"INT2": true, "INT4": true, "INT8": true, "FLOAT4": true, "FLOAT8": true, "NUMERIC": true, "OID": true,
|
||||
}
|
||||
|
||||
var postgresIntegerTypes = map[string]bool{"INT2": true, "INT4": true, "INT8": true}
|
||||
|
||||
var postgresSerialTypes = map[string]string{
|
||||
"smallint": "smallserial",
|
||||
"integer": "serial",
|
||||
@@ -175,7 +177,7 @@ func postgresLiteral(databaseType string, value []byte) string {
|
||||
|
||||
func postgresSequenceReset(table string, columns []DumpColumn) []string {
|
||||
for _, column := range columns {
|
||||
if column.Name != postgresIdentityColumn {
|
||||
if column.Name != postgresIdentityColumn || !postgresIntegerTypes[strings.ToUpper(column.Type)] {
|
||||
continue
|
||||
}
|
||||
quotedTable, quotedColumn := quotePostgresIdentifier(table), quotePostgresIdentifier(column.Name)
|
||||
|
||||
@@ -46,6 +46,9 @@ func (a *ArticleRepository) scoped(filters domain.Filters) *gorm.DB {
|
||||
if filters.SiteID != nil {
|
||||
query = query.Where("site_id = ?", *filters.SiteID)
|
||||
}
|
||||
if filters.VisibleTo != nil {
|
||||
query = query.Where("(is_online = ? OR created_by = ?)", true, *filters.VisibleTo)
|
||||
}
|
||||
switch filters.Status {
|
||||
case domain.StatusOnline:
|
||||
query = query.Where("is_online = ?", true)
|
||||
@@ -168,6 +171,17 @@ func (a *ArticleRepository) CountByVisibility(onlineOnly bool) (domain.Counts, e
|
||||
return domain.Counts{All: int(online + draft), Online: int(online), Draft: int(draft)}, nil
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) CountVisibleTo(userID uint32) (domain.Counts, error) {
|
||||
var online, draft int64
|
||||
if err := a.db.Model(&entity.Article{}).Where("is_online = ?", true).Count(&online).Error; err != nil {
|
||||
return domain.Counts{}, err
|
||||
}
|
||||
if err := a.db.Model(&entity.Article{}).Where("is_online = ? AND created_by = ?", false, userID).Count(&draft).Error; err != nil {
|
||||
return domain.Counts{}, err
|
||||
}
|
||||
return domain.Counts{All: int(online + draft), Online: int(online), Draft: int(draft)}, nil
|
||||
}
|
||||
|
||||
func toDomainArticles(entityArticles []entity.Article) []domain.Article {
|
||||
domainArticles := make([]domain.Article, 0, len(entityArticles))
|
||||
for _, entityArticle := range entityArticles {
|
||||
@@ -356,11 +370,11 @@ func (a *ArticleRepository) SlugTaken(siteID uint32, slug string, exceptID uint3
|
||||
return articleSlugInUse(a.db, siteID, slug, exceptID)
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) GetAll() []domain.Article {
|
||||
func (a *ArticleRepository) GetAll() ([]domain.Article, error) {
|
||||
var articles []entity.Article
|
||||
err := a.db.Model(&entity.Article{}).Find(&articles).Error
|
||||
if err != nil {
|
||||
return []domain.Article{}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var domainArticles = make([]domain.Article, 0)
|
||||
@@ -368,7 +382,7 @@ func (a *ArticleRepository) GetAll() []domain.Article {
|
||||
domainArticles = append(domainArticles, mappers.ArticleToDomain(article))
|
||||
}
|
||||
|
||||
return domainArticles
|
||||
return domainArticles, nil
|
||||
}
|
||||
|
||||
func (a *ArticleRepository) UpdateBody(id uint32, body string) (domain.Article, error) {
|
||||
|
||||
@@ -71,7 +71,11 @@ func (e *EntryRepository) ListPage(filters domain.Filters) (domain.PageResult, e
|
||||
filters.Page = max(filters.Page, 1)
|
||||
filters.Size = max(filters.Size, 0)
|
||||
scoped := func() *gorm.DB {
|
||||
return e.db.Model(&entity.Entry{}).Where("content_type_id = ?", filters.ContentTypeID)
|
||||
query := e.db.Model(&entity.Entry{}).Where("content_type_id = ?", filters.ContentTypeID)
|
||||
if filters.CreatedBy != nil {
|
||||
query = query.Where("created_by = ?", *filters.CreatedBy)
|
||||
}
|
||||
return query
|
||||
}
|
||||
var total int64
|
||||
if err := scoped().Count(&total).Error; err != nil {
|
||||
|
||||
@@ -28,16 +28,18 @@ func (i *InviteRepository) Create(invite domainInvite.Invite) (domainInvite.Invi
|
||||
return mappers.InviteToDomain(model), nil
|
||||
}
|
||||
|
||||
func (i *InviteRepository) GetAll() []domainInvite.Invite {
|
||||
func (i *InviteRepository) GetAll() ([]domainInvite.Invite, error) {
|
||||
var invites []entity.Invite
|
||||
i.db.Model(&entity.Invite{}).Find(&invites)
|
||||
if err := i.db.Model(&entity.Invite{}).Find(&invites).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var domainInvites []domainInvite.Invite
|
||||
for _, localInvite := range invites {
|
||||
domainInvites = append(domainInvites, mappers.InviteToDomain(localInvite))
|
||||
}
|
||||
|
||||
return domainInvites
|
||||
return domainInvites, nil
|
||||
}
|
||||
|
||||
func (i *InviteRepository) Delete(id uint32) error {
|
||||
|
||||
@@ -52,16 +52,18 @@ func (u *UserRepository) Delete(id uint32) error {
|
||||
return deleteOrNotFound(result, domainUser.ErrUserNotFound)
|
||||
}
|
||||
|
||||
func (u *UserRepository) GetAll() []domainUser.User {
|
||||
func (u *UserRepository) GetAll() ([]domainUser.User, error) {
|
||||
var users []entity.User
|
||||
u.db.Model(&entity.User{}).Find(&users)
|
||||
if err := u.db.Model(&entity.User{}).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var domainUsers []domainUser.User
|
||||
for _, localUser := range users {
|
||||
domainUsers = append(domainUsers, mappers.UserToDomain(localUser))
|
||||
}
|
||||
|
||||
return domainUsers
|
||||
return domainUsers, nil
|
||||
}
|
||||
|
||||
func (u *UserRepository) FindByIDs(ids []uint32) ([]domainUser.User, error) {
|
||||
|
||||
@@ -72,7 +72,11 @@ func (a *AcceptInviteUseCase) recordRefusal(ctx context.Context, event audit.Eve
|
||||
}
|
||||
|
||||
func (a *AcceptInviteUseCase) accept(command AcceptInviteCommand, pending invite.Invite, assigned role.Role, now time.Time) (user.User, error) {
|
||||
if a.emailHasAccount(pending.Email) {
|
||||
hasAccount, err := a.emailHasAccount(pending.Email)
|
||||
if err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
if hasAccount {
|
||||
if _, err := findUsableInvite(a.inviteRepository, a.hasher, command.Code, now); err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
@@ -122,13 +126,17 @@ func (a *AcceptInviteUseCase) inviterOf(pending invite.Invite) (uint32, error) {
|
||||
return assigner.ID, nil
|
||||
}
|
||||
|
||||
func (a *AcceptInviteUseCase) emailHasAccount(email string) bool {
|
||||
for _, existingUser := range a.userRepository.GetAll() {
|
||||
func (a *AcceptInviteUseCase) emailHasAccount(email string) (bool, error) {
|
||||
users, err := a.userRepository.GetAll()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, existingUser := range users {
|
||||
if strings.EqualFold(existingUser.Email, email) {
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (a *AcceptInviteUseCase) ensureUsernameFree(username string) error {
|
||||
|
||||
@@ -98,7 +98,11 @@ func (g *CreateInviteUseCase) create(command CreateInviteCommand) (invite.Invite
|
||||
return invite.Invite{}, role.Role{}, err
|
||||
}
|
||||
|
||||
if g.emailHasAccount(email) {
|
||||
hasAccount, err := g.emailHasAccount(email)
|
||||
if err != nil {
|
||||
return invite.Invite{}, role.Role{}, err
|
||||
}
|
||||
if hasAccount {
|
||||
return invite.Invite{}, role.Role{}, invite.ErrEmailHasAccount
|
||||
}
|
||||
|
||||
@@ -133,17 +137,25 @@ func (g *CreateInviteUseCase) create(command CreateInviteCommand) (invite.Invite
|
||||
return createdInvite, assigned, nil
|
||||
}
|
||||
|
||||
func (g *CreateInviteUseCase) emailHasAccount(email string) bool {
|
||||
for _, existingUser := range g.userRepository.GetAll() {
|
||||
func (g *CreateInviteUseCase) emailHasAccount(email string) (bool, error) {
|
||||
users, err := g.userRepository.GetAll()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, existingUser := range users {
|
||||
if strings.EqualFold(existingUser.Email, email) {
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (g *CreateInviteUseCase) discardOpenInvite(email string, now time.Time) error {
|
||||
for _, existing := range g.inviteRepository.GetAll() {
|
||||
invites, err := g.inviteRepository.GetAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, existing := range invites {
|
||||
if existing.Email != email {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ func (g *CreateUserUseCase) CreateUser(ctx context.Context, createUser CreateUse
|
||||
}
|
||||
|
||||
func (g *CreateUserUseCase) create(createUser CreateUserCommand) (user.User, error) {
|
||||
if g.registrationClosed() {
|
||||
closed, err := g.registrationClosed()
|
||||
if err != nil {
|
||||
return user.User{}, err
|
||||
}
|
||||
if closed {
|
||||
return user.User{}, user.ErrRegistrationClosed
|
||||
}
|
||||
|
||||
@@ -108,15 +112,20 @@ func (g *CreateUserUseCase) create(createUser CreateUserCommand) (user.User, err
|
||||
return createdUser, nil
|
||||
}
|
||||
|
||||
func (g *CreateUserUseCase) IsRegistrationOpen() bool {
|
||||
return !g.registrationClosed()
|
||||
func (g *CreateUserUseCase) IsRegistrationOpen() (bool, error) {
|
||||
closed, err := g.registrationClosed()
|
||||
return !closed, err
|
||||
}
|
||||
|
||||
func (g *CreateUserUseCase) registrationClosed() bool {
|
||||
for _, existingUser := range g.userRepository.GetAll() {
|
||||
func (g *CreateUserUseCase) registrationClosed() (bool, error) {
|
||||
users, err := g.userRepository.GetAll()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, existingUser := range users {
|
||||
if existingUser.IsVerified {
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
@@ -34,7 +34,11 @@ func (e *EnforceRetentionUseCase) Tick(now time.Time) (retention.Result, error)
|
||||
|
||||
func (e *EnforceRetentionUseCase) deleteStaleUnverifiedUsers(now time.Time) (int, error) {
|
||||
removed := 0
|
||||
for _, candidate := range e.users.GetAll() {
|
||||
users, err := e.users.GetAll()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, candidate := range users {
|
||||
if !candidate.IsStaleUnverifiedAt(now) {
|
||||
continue
|
||||
}
|
||||
@@ -48,7 +52,11 @@ func (e *EnforceRetentionUseCase) deleteStaleUnverifiedUsers(now time.Time) (int
|
||||
|
||||
func (e *EnforceRetentionUseCase) deleteStaleInvites(now time.Time) (int, error) {
|
||||
removed := 0
|
||||
for _, candidate := range e.invites.GetAll() {
|
||||
invites, err := e.invites.GetAll()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, candidate := range invites {
|
||||
if !candidate.IsStaleAt(now) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ func findUsableInvite(inviteRepository invite.Repository, hasher hash.Hasher, co
|
||||
return invite.Invite{}, invite.ErrInviteInvalid
|
||||
}
|
||||
|
||||
for _, candidate := range inviteRepository.GetAll() {
|
||||
invites, err := inviteRepository.GetAll()
|
||||
if err != nil {
|
||||
return invite.Invite{}, err
|
||||
}
|
||||
for _, candidate := range invites {
|
||||
if candidate.AcceptedAt != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -42,20 +42,42 @@ type ArticlePage struct {
|
||||
func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (ArticlePage, error) {
|
||||
switch command.Caller.Grants.ScopeFor(role.KeyContentRead, builtintype.KeyArticle) {
|
||||
case role.ScopeAll:
|
||||
return u.listVisibleByRepository(command, false)
|
||||
return u.listVisibleByRepository(command, articleVisibility{})
|
||||
case role.ScopeOwn:
|
||||
return u.listOwnedAndOnline(command)
|
||||
return u.listVisibleByRepository(command, articleVisibility{userID: &command.Caller.UserID})
|
||||
default:
|
||||
return u.listVisibleByRepository(command, true)
|
||||
return u.listVisibleByRepository(command, articleVisibility{onlineOnly: true})
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) listVisibleByRepository(command ListArticlesCommand, onlineOnly bool) (ArticlePage, error) {
|
||||
counts, err := u.articleRepository.CountByVisibility(onlineOnly)
|
||||
type articleVisibility struct {
|
||||
onlineOnly bool
|
||||
userID *uint32
|
||||
}
|
||||
|
||||
func (v articleVisibility) apply(filters article.Filters) article.Filters {
|
||||
if v.onlineOnly {
|
||||
online := true
|
||||
filters.IsOnline = &online
|
||||
}
|
||||
filters.VisibleTo = v.userID
|
||||
return filters
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) countVisible(visibility articleVisibility) (article.Counts, error) {
|
||||
if visibility.userID != nil {
|
||||
return u.articleRepository.CountVisibleTo(*visibility.userID)
|
||||
}
|
||||
return u.articleRepository.CountByVisibility(visibility.onlineOnly)
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) listVisibleByRepository(command ListArticlesCommand, visibility articleVisibility) (ArticlePage, error) {
|
||||
onlineOnly := visibility.onlineOnly
|
||||
counts, err := u.countVisible(visibility)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
summaries, err := u.summarizeVisible(onlineOnly)
|
||||
summaries, err := u.summarizeVisible(visibility)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
@@ -64,7 +86,7 @@ func (u *ListArticlesUseCase) listVisibleByRepository(command ListArticlesComman
|
||||
if onlineOnly && command.Status == article.StatusDraft {
|
||||
return page, nil
|
||||
}
|
||||
filters := pageFilters(command, onlineOnly)
|
||||
filters := visibility.apply(pageFilters(command))
|
||||
if command.SiteKey != nil {
|
||||
siteID, known, err := u.siteIDOf(*command.SiteKey)
|
||||
if err != nil {
|
||||
@@ -98,71 +120,6 @@ func emptyArticlePage(command ListArticlesCommand, counts article.Counts) Articl
|
||||
return ArticlePage{Items: []article.Article{}, Page: command.Page, Size: command.Size, Counts: counts, SEO: map[uint32]article.SEOSummary{}}
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) listOwnedAndOnline(command ListArticlesCommand) (ArticlePage, error) {
|
||||
everything, err := u.articleRepository.FindPage(article.Filters{Unpaged: true})
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
visible := visibleTo(command.Caller, everything.Items)
|
||||
summaries, err := u.summarize(visible)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
counts := tally(visible)
|
||||
counts.Suggestions = countSuggestions(summaries)
|
||||
page := emptyArticlePage(command, counts)
|
||||
filters := pageFilters(command, false)
|
||||
filters.Unpaged = true
|
||||
if command.SiteKey != nil {
|
||||
siteID, known, err := u.siteIDOf(*command.SiteKey)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
if !known {
|
||||
return page, nil
|
||||
}
|
||||
filters.SiteID = &siteID
|
||||
}
|
||||
found, err := u.articleRepository.FindPage(filters)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
matching := visibleTo(command.Caller, found.Items)
|
||||
if command.SEO != "" {
|
||||
matching = withSEOStatus(matching, summaries, command.SEO)
|
||||
}
|
||||
rendered, err := renderArticles(u.bodyRenderer, pageSlice(matching, command.Page, command.Size), command.Format)
|
||||
if err != nil {
|
||||
return ArticlePage{}, err
|
||||
}
|
||||
page.Items = rendered
|
||||
page.Total = len(matching)
|
||||
page.SEO = summaries
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func visibleTo(caller ContentCaller, items []article.Article) []article.Article {
|
||||
visible := make([]article.Article, 0, len(items))
|
||||
for _, item := range items {
|
||||
if caller.seesArticle(item) {
|
||||
visible = append(visible, item)
|
||||
}
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
func tally(visible []article.Article) article.Counts {
|
||||
counts := article.Counts{All: len(visible)}
|
||||
for _, item := range visible {
|
||||
if item.IsOnline {
|
||||
counts.Online++
|
||||
} else {
|
||||
counts.Draft++
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
func withSEOStatus(items []article.Article, summaries map[uint32]article.SEOSummary, status string) []article.Article {
|
||||
matching := make([]article.Article, 0, len(items))
|
||||
for _, item := range items {
|
||||
@@ -187,13 +144,8 @@ func pageSlice[T any](items []T, page int, size int) []T {
|
||||
return items[start:min(start+size, len(items))]
|
||||
}
|
||||
|
||||
func (u *ListArticlesUseCase) summarizeVisible(onlineOnly bool) (map[uint32]article.SEOSummary, error) {
|
||||
filters := article.Filters{Unpaged: true}
|
||||
if onlineOnly {
|
||||
online := true
|
||||
filters.IsOnline = &online
|
||||
}
|
||||
found, err := u.articleRepository.FindPage(filters)
|
||||
func (u *ListArticlesUseCase) summarizeVisible(visibility articleVisibility) (map[uint32]article.SEOSummary, error) {
|
||||
found, err := u.articleRepository.FindPage(visibility.apply(article.Filters{Unpaged: true}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -245,11 +197,6 @@ func (u *ListArticlesUseCase) siteIDOf(key string) (uint32, bool, error) {
|
||||
return found.ID, true, nil
|
||||
}
|
||||
|
||||
func pageFilters(command ListArticlesCommand, onlineOnly bool) article.Filters {
|
||||
filters := article.Filters{Page: command.Page, Size: command.Size, Q: command.Q, Status: command.Status, Sort: command.Sort}
|
||||
if onlineOnly {
|
||||
online := true
|
||||
filters.IsOnline = &online
|
||||
}
|
||||
return filters
|
||||
func pageFilters(command ListArticlesCommand) article.Filters {
|
||||
return article.Filters{Page: command.Page, Size: command.Size, Q: command.Q, Status: command.Status, Sort: command.Sort}
|
||||
}
|
||||
@@ -70,7 +70,14 @@ func (u *ListEntriesUseCase) page(owner contenttype.ContentType, command ListEnt
|
||||
if query == "" && readScope == role.ScopeAll {
|
||||
return u.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, Page: command.Page, Size: command.Size, Sort: command.Sort})
|
||||
}
|
||||
all, err := u.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, Unpaged: true, Sort: command.Sort})
|
||||
if query == "" && readScope == role.ScopeOwn {
|
||||
return u.entries.ListPage(entry.Filters{ContentTypeID: owner.ID, CreatedBy: &command.Caller.UserID, Page: command.Page, Size: command.Size, Sort: command.Sort})
|
||||
}
|
||||
filters := entry.Filters{ContentTypeID: owner.ID, Unpaged: true, Sort: command.Sort}
|
||||
if readScope == role.ScopeOwn {
|
||||
filters.CreatedBy = &command.Caller.UserID
|
||||
}
|
||||
all, err := u.entries.ListPage(filters)
|
||||
if err != nil {
|
||||
return entry.PageResult{}, err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ func NewListUsersUseCase(userRepository user.Repository) *ListUsersUseCase {
|
||||
return &ListUsersUseCase{userRepository}
|
||||
}
|
||||
|
||||
func (g *ListUsersUseCase) ListUsers() []user.User {
|
||||
func (g *ListUsersUseCase) ListUsers() ([]user.User, error) {
|
||||
return g.userRepository.GetAll()
|
||||
}
|
||||
@@ -21,8 +21,8 @@ type ListUsersAndInvitesUseCase struct {
|
||||
}
|
||||
|
||||
type RoleAssigner struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID uint32
|
||||
Name string
|
||||
}
|
||||
|
||||
func newRoleAssigner(account user.User) *RoleAssigner {
|
||||
@@ -89,7 +89,10 @@ func (g *ListUsersAndInvitesUseCase) ListUsersAndInvites() ([]DirectoryEntry, er
|
||||
now := time.Now()
|
||||
entries := []DirectoryEntry{}
|
||||
|
||||
users := g.userRepository.GetAll()
|
||||
users, err := g.userRepository.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loaded := make(map[uint32]user.User, len(users))
|
||||
for _, existingUser := range users {
|
||||
loaded[existingUser.ID] = existingUser
|
||||
@@ -98,7 +101,11 @@ func (g *ListUsersAndInvitesUseCase) ListUsersAndInvites() ([]DirectoryEntry, er
|
||||
entries = append(entries, userEntry(existingUser, lookupRoleRef(refs, existingUser.RoleID), assignerAmong(loaded, existingUser.RoleAssignedByID)))
|
||||
}
|
||||
|
||||
for _, existingInvite := range g.inviteRepository.GetAll() {
|
||||
invites, err := g.inviteRepository.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, existingInvite := range invites {
|
||||
status := existingInvite.StatusAt(now)
|
||||
if status == invite.StatusAccepted {
|
||||
continue
|
||||
|
||||
@@ -21,19 +21,19 @@ type TransferOwnershipCommand struct {
|
||||
}
|
||||
|
||||
type OwnershipSuccessor struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID uint32
|
||||
Name string
|
||||
}
|
||||
|
||||
type OwnershipPredecessor struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Role RoleRef `json:"role"`
|
||||
ID uint32
|
||||
Name string
|
||||
Role RoleRef
|
||||
}
|
||||
|
||||
type OwnershipTransfer struct {
|
||||
Owner OwnershipSuccessor `json:"owner"`
|
||||
FormerOwner OwnershipPredecessor `json:"former_owner"`
|
||||
Owner OwnershipSuccessor
|
||||
FormerOwner OwnershipPredecessor
|
||||
}
|
||||
|
||||
func NewTransferOwnershipUseCase(users user.Repository, roles role.Repository, recorder audit.Recorder) *TransferOwnershipUseCase {
|
||||
|
||||
@@ -3,7 +3,11 @@ package useCases
|
||||
import "RenewCMS/internal/domain/invite"
|
||||
|
||||
func findManageableInvite(inviteRepository invite.Repository, id uint32) (invite.Invite, error) {
|
||||
for _, existing := range inviteRepository.GetAll() {
|
||||
invites, err := inviteRepository.GetAll()
|
||||
if err != nil {
|
||||
return invite.Invite{}, err
|
||||
}
|
||||
for _, existing := range invites {
|
||||
if existing.ID != id {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ type InviteView struct {
|
||||
}
|
||||
|
||||
type RoleRef struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
BuiltIn bool `json:"built_in"`
|
||||
Locked bool `json:"locked"`
|
||||
ID uint32
|
||||
Name string
|
||||
BuiltIn bool
|
||||
Locked bool
|
||||
}
|
||||
|
||||
func NewRoleRef(found role.Role) RoleRef {
|
||||
|
||||
@@ -9,15 +9,15 @@ import (
|
||||
)
|
||||
|
||||
type RoleEditor struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ID uint32
|
||||
Name string
|
||||
}
|
||||
|
||||
type RoleView struct {
|
||||
role.Role
|
||||
Members int `json:"members"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
UpdatedBy *RoleEditor `json:"updated_by"`
|
||||
Members int
|
||||
UpdatedAt *time.Time
|
||||
UpdatedBy *RoleEditor
|
||||
}
|
||||
|
||||
type roleViewer struct {
|
||||
|
||||
@@ -43,10 +43,10 @@ type atomicInviteRepoDecorator struct {
|
||||
afterGetAll func()
|
||||
}
|
||||
|
||||
func (d *atomicInviteRepoDecorator) GetAll() []invite.Invite {
|
||||
all := d.Repository.GetAll()
|
||||
func (d *atomicInviteRepoDecorator) GetAll() ([]invite.Invite, error) {
|
||||
all, err := d.Repository.GetAll()
|
||||
d.afterGetAll()
|
||||
return all
|
||||
return all, err
|
||||
}
|
||||
|
||||
type atomicLookupBarrier struct {
|
||||
|
||||
@@ -50,39 +50,15 @@ func routerRoutes(t *testing.T) map[string]bool {
|
||||
}
|
||||
|
||||
var routesNotYetInTheCatalogue = []string{
|
||||
"DELETE /v1/content-type/{key}",
|
||||
"DELETE /v1/content-type/{key}/entry/{id}",
|
||||
"DELETE /v1/image-variants/{name}",
|
||||
"DELETE /v1/media/{id}/variants/{name}",
|
||||
"DELETE /v1/role/{id}",
|
||||
"GET /v1/builtin-type",
|
||||
"GET /v1/builtin-type/{key}",
|
||||
"GET /v1/content-type",
|
||||
"GET /v1/content-type/{key}",
|
||||
"GET /v1/content-type/{key}/entry",
|
||||
"GET /v1/content-type/{key}/entry/{id}",
|
||||
"GET /v1/image-variants",
|
||||
"GET /v1/image-variants/cache",
|
||||
"GET /v1/media/{id}/variants",
|
||||
"GET /v1/user/lookup",
|
||||
"GET /v1/permission",
|
||||
"GET /v1/role",
|
||||
"GET /v1/role/{id}",
|
||||
"PATCH /v1/image-variants/{name}",
|
||||
"POST /v1/content-type",
|
||||
"POST /v1/content-type/{key}/entry",
|
||||
"POST /v1/image-variants",
|
||||
"POST /v1/image-variants/cache/purge",
|
||||
"POST /v1/image-variants/{name}/purge",
|
||||
"POST /v1/media/{id}/variants",
|
||||
"POST /v1/media/{id}/variants/{name}",
|
||||
"POST /v1/role",
|
||||
"POST /v1/role/{id}/reassign",
|
||||
"POST /v1/role/{id}/reset",
|
||||
"POST /v1/site/transfer-ownership",
|
||||
"PUT /v1/content-type/{key}",
|
||||
"PUT /v1/content-type/{key}/entry/{id}",
|
||||
"PUT /v1/image-variants/{name}",
|
||||
"PUT /v1/role/{id}",
|
||||
"PUT /v1/user/{id}/role",
|
||||
}
|
||||
|
||||
+21
-2
@@ -92,6 +92,9 @@ func (m *mockArticleRepo) FindPage(filters article.Filters) (article.PageResult,
|
||||
if filters.SiteID != nil && a.SiteID != *filters.SiteID {
|
||||
continue
|
||||
}
|
||||
if filters.VisibleTo != nil && !a.IsOnline && a.CreatedBy != *filters.VisibleTo {
|
||||
continue
|
||||
}
|
||||
if filters.Status == article.StatusOnline && !a.IsOnline {
|
||||
continue
|
||||
}
|
||||
@@ -132,6 +135,22 @@ func (m *mockArticleRepo) CountByVisibility(onlineOnly bool) (article.Counts, er
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) CountVisibleTo(userID uint32) (article.Counts, error) {
|
||||
if err := m.fail.trigger("CountVisibleTo"); err != nil {
|
||||
return article.Counts{}, err
|
||||
}
|
||||
counts := article.Counts{}
|
||||
for _, a := range m.articles {
|
||||
if a.IsOnline {
|
||||
counts.Online++
|
||||
} else if a.CreatedBy == userID {
|
||||
counts.Draft++
|
||||
}
|
||||
}
|
||||
counts.All = counts.Online + counts.Draft
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func articleSortsBefore(left, right article.Article, order article.Sort) bool {
|
||||
if order.Field == "" {
|
||||
order = article.DefaultSort()
|
||||
@@ -195,12 +214,12 @@ func (m *mockArticleRepo) GetByName(name string) (article.Article, error) {
|
||||
return article.Article{}, article.ErrArticleNotFound
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) GetAll() []article.Article {
|
||||
func (m *mockArticleRepo) GetAll() ([]article.Article, error) {
|
||||
result := make([]article.Article, 0)
|
||||
for _, id := range m.sortedIds() {
|
||||
result = append(result, m.articles[id])
|
||||
}
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) slugUsedOn(siteID uint32, slug string, exceptID uint32) bool {
|
||||
|
||||
@@ -146,7 +146,7 @@ func (m *mockEntryRepo) ListPage(filters entry.Filters) (entry.PageResult, error
|
||||
}
|
||||
matching := []entry.Entry{}
|
||||
for _, stored := range m.entries {
|
||||
if stored.ContentTypeID == filters.ContentTypeID {
|
||||
if stored.ContentTypeID == filters.ContentTypeID && (filters.CreatedBy == nil || stored.CreatedBy == *filters.CreatedBy) {
|
||||
matching = append(matching, stored)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetAllFailuresReachTheCaller(t *testing.T) {
|
||||
boom := errors.New("storage down")
|
||||
|
||||
t.Run("registration status answers 500 instead of reporting open", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.fail.failNextWith("GetAll", boom)
|
||||
router, _ := newRegistrationStatusRouter(userRepo, newMockMailRepo())
|
||||
|
||||
rec := registrationStatusRequest(router, nil)
|
||||
|
||||
if rec.Code != http.StatusInternalServerError {
|
||||
t.Errorf("status = %d, want 500", rec.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IsRegistrationOpen returns the error", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.fail.failNextWith("GetAll", boom)
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, newSeededMockRoleRepo(), &stubHasher{}, newMockMailRepo(), "https://cms.example.com", discardRecorder)
|
||||
|
||||
if _, err := uc.IsRegistrationOpen(); !errors.Is(err, boom) {
|
||||
t.Errorf("err = %v, want %v", err, boom)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetInvite returns the error instead of an invalid invite", func(t *testing.T) {
|
||||
invites := newInviteFakeRepo()
|
||||
invites.fail.failNextWith("GetAll", boom)
|
||||
uc := useCases.NewGetInviteUseCase(invites, &stubHasher{})
|
||||
|
||||
if _, err := uc.GetInvite("code"); !errors.Is(err, boom) {
|
||||
t.Errorf("err = %v, want %v", err, boom)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListUsers returns the error", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.fail.failNextWith("GetAll", boom)
|
||||
|
||||
if _, err := useCases.NewListUsersUseCase(userRepo).ListUsers(); !errors.Is(err, boom) {
|
||||
t.Errorf("err = %v, want %v", err, boom)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("retention stops on a user listing failure", func(t *testing.T) {
|
||||
userRepo := newMockUserRepo()
|
||||
userRepo.fail.failNextWith("GetAll", boom)
|
||||
uc := useCases.NewEnforceRetentionUseCase(userRepo, newInviteFakeRepo(), &memorySessionRepo{})
|
||||
|
||||
if _, err := uc.Tick(time.Now()); !errors.Is(err, boom) {
|
||||
t.Errorf("err = %v, want %v", err, boom)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("retention stops on an invite listing failure", func(t *testing.T) {
|
||||
invites := newInviteFakeRepo()
|
||||
invites.fail.failNextWith("GetAll", boom)
|
||||
uc := useCases.NewEnforceRetentionUseCase(newMockUserRepo(), invites, &memorySessionRepo{})
|
||||
|
||||
if _, err := uc.Tick(time.Now()); !errors.Is(err, boom) {
|
||||
t.Errorf("err = %v, want %v", err, boom)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -52,7 +52,10 @@ func (m *inviteFakeRepo) Create(i invite.Invite) (invite.Invite, error) {
|
||||
return m.seed(i), nil
|
||||
}
|
||||
|
||||
func (m *inviteFakeRepo) GetAll() []invite.Invite {
|
||||
func (m *inviteFakeRepo) GetAll() ([]invite.Invite, error) {
|
||||
if err := m.fail.trigger("GetAll"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]uint32, 0, len(m.invites))
|
||||
for id := range m.invites {
|
||||
ids = append(ids, id)
|
||||
@@ -62,7 +65,7 @@ func (m *inviteFakeRepo) GetAll() []invite.Invite {
|
||||
for _, id := range ids {
|
||||
result = append(result, m.invites[id])
|
||||
}
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *inviteFakeRepo) Delete(id uint32) error {
|
||||
|
||||
@@ -59,8 +59,9 @@ func TestMySQLMigration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
binaryTables := columnCount(t, db, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_collation = 'utf8mb4_bin'")
|
||||
if binaryTables != 6 {
|
||||
t.Errorf("every table must use the utf8mb4_bin collation, got %d of 6", binaryTables)
|
||||
allTables := columnCount(t, db, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'")
|
||||
if binaryTables != allTables {
|
||||
t.Errorf("every table must use the utf8mb4_bin collation, got %d of %d", binaryTables, allTables)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -247,7 +248,7 @@ func TestMySQLBackup(t *testing.T) {
|
||||
if line := firstDifferingLine(withoutRedundantCharset(dump), withoutRedundantCharset(string(restoredContent))); line != "" {
|
||||
t.Errorf("replaying the dump must restore the same schema and rows, first difference: %s", line)
|
||||
}
|
||||
restoredUsers := newContractRepositories(restored.DB).users.GetAll()
|
||||
restoredUsers := allOf(newContractRepositories(restored.DB).users.GetAll())
|
||||
if len(restoredUsers) != 2 || restoredUsers[0].Username != "o'brien \"quoted\" back\\slash\nline é😀" || restoredUsers[1].PasswordResetCode != "reset-code" {
|
||||
t.Errorf("unexpected restored users %+v", restoredUsers)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/infrastructure/persistence/database"
|
||||
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArticleRepositoryVisibilityFilterOnSQLite(t *testing.T) {
|
||||
db := openContractSQLite(t)
|
||||
repo := persistence.NewArticleRepository(db)
|
||||
defaultSite, err := persistence.NewSiteRepository(db).GetDefault()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
create := func(owner uint32, online bool) {
|
||||
t.Helper()
|
||||
record := articleOn(defaultSite.ID)
|
||||
record.CreatedBy = owner
|
||||
record.IsOnline = online
|
||||
if _, err := repo.Create(record); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
create(1, true)
|
||||
create(1, false)
|
||||
create(2, true)
|
||||
create(2, false)
|
||||
create(2, false)
|
||||
one := uint32(1)
|
||||
|
||||
t.Run("FindPage keeps online articles and the viewer's drafts", func(t *testing.T) {
|
||||
found, err := repo.FindPage(article.Filters{VisibleTo: &one, Unpaged: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if found.Total != 3 || len(found.Items) != 3 {
|
||||
t.Fatalf("total = %d, items = %d, want 3", found.Total, len(found.Items))
|
||||
}
|
||||
for _, item := range found.Items {
|
||||
if !item.IsOnline && item.CreatedBy != one {
|
||||
t.Errorf("leaked draft %+v", item)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FindPage pages the filtered set", func(t *testing.T) {
|
||||
found, err := repo.FindPage(article.Filters{VisibleTo: &one, Page: 2, Size: 2})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if found.Total != 3 || len(found.Items) != 1 {
|
||||
t.Fatalf("total = %d, items = %d, want 3 and 1", found.Total, len(found.Items))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FindPage combines the filter with the draft status", func(t *testing.T) {
|
||||
found, err := repo.FindPage(article.Filters{VisibleTo: &one, Status: article.StatusDraft, Unpaged: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if found.Total != 1 || found.Items[0].CreatedBy != one {
|
||||
t.Fatalf("found = %+v", found)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CountVisibleTo counts online articles and the viewer's drafts", func(t *testing.T) {
|
||||
counts, err := repo.CountVisibleTo(one)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if counts != (article.Counts{All: 3, Online: 2, Draft: 1}) {
|
||||
t.Fatalf("counts = %+v", counts)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEntryRepositoryCreatedByFilterOnSQLite(t *testing.T) {
|
||||
db := openUnmigratedSQLite(t)
|
||||
if err := database.Migrate(db, migrationPublicURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
types := persistence.NewContentTypeRepository(db)
|
||||
entries := persistence.NewEntryRepository(db)
|
||||
created, err := types.Create(contenttype.ContentType{Key: "notes", Name: "Notes", Fields: []contenttype.Field{{Key: "flag", Label: "Flag", Type: contenttype.TypeBoolean}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, owner := range []uint32{1, 2, 1, 2, 1} {
|
||||
if _, err := entries.Create(entry.Entry{ContentTypeID: created.ID, Data: map[string]any{}, CreatedBy: owner, UpdatedBy: owner}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
one := uint32(1)
|
||||
|
||||
found, err := entries.ListPage(entry.Filters{ContentTypeID: created.ID, CreatedBy: &one, Page: 1, Size: 2, Sort: entry.Sort{Field: entry.SortByID, Direction: entry.SortAsc}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if found.Total != 3 || len(found.Items) != 2 {
|
||||
t.Fatalf("total = %d, items = %d, want 3 and 2", found.Total, len(found.Items))
|
||||
}
|
||||
for _, item := range found.Items {
|
||||
if item.CreatedBy != one {
|
||||
t.Errorf("leaked entry %+v", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func TestEnforceRetentionUseCase(t *testing.T) {
|
||||
if _, removed := w.users.users[2]; removed {
|
||||
t.Error("the abandoned account must be removed")
|
||||
}
|
||||
remaining := w.invites.GetAll()
|
||||
remaining := allOf(w.invites.GetAll())
|
||||
if len(remaining) != 1 || remaining[0].Email != "pending@example.com" {
|
||||
t.Errorf("expected only the pending invite kept, got %+v", remaining)
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ func TestPostgresMigration(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("a legacy users.role column is backfilled into role_id and the invites get Admin", func(t *testing.T) {
|
||||
_, _, db := migratedPostgres(t)
|
||||
server, config, db := migratedPostgres(t)
|
||||
addLegacyRoleColumn(t, db)
|
||||
for _, name := range []string{"first", "second", "third"} {
|
||||
insert := db.Exec("INSERT INTO users (username, password, email, role, is_verified, token_version) VALUES (?, 'x', ?, '', true, 0)", name, name+"@x.example")
|
||||
@@ -193,6 +193,7 @@ func TestPostgresMigration(t *testing.T) {
|
||||
t.Fatalf("insert %s: %v", name, insert.Error)
|
||||
}
|
||||
}
|
||||
db = server.connect(t, config).DB
|
||||
|
||||
if err := database.Migrate(db, migrationPublicURL); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
@@ -241,7 +242,7 @@ func TestPostgresTimesInLists(t *testing.T) {
|
||||
mustCreateUser(t, repos, user)
|
||||
}
|
||||
|
||||
users := repos.users.GetAll()
|
||||
users := allOf(repos.users.GetAll())
|
||||
|
||||
if len(users) != 2 {
|
||||
t.Fatalf("expected 2 users, got %d", len(users))
|
||||
@@ -402,7 +403,7 @@ func TestPostgresBackup(t *testing.T) {
|
||||
t.Errorf("replaying the dump must restore the same schema and rows, first difference: %s", line)
|
||||
}
|
||||
restoredRepos := newContractRepositories(restored.DB)
|
||||
restoredUsers := restoredRepos.users.GetAll()
|
||||
restoredUsers := allOf(restoredRepos.users.GetAll())
|
||||
if len(restoredUsers) != 2 || restoredUsers[0].Username != "o'brien \"quoted\" back\\slash\nline é😀" || restoredUsers[1].PasswordResetCode != "reset-code" {
|
||||
t.Errorf("unexpected restored users %+v", restoredUsers)
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestRegistrationStatusEndpointHttp(t *testing.T) {
|
||||
mailRepo := newMockMailRepo()
|
||||
uc := useCases.NewCreateUserUseCase(userRepo, newSeededMockRoleRepo(), &stubHasher{}, mailRepo, "https://cms.example.com", discardRecorder)
|
||||
|
||||
if uc.IsRegistrationOpen() {
|
||||
if open, openErr := uc.IsRegistrationOpen(); openErr != nil || open {
|
||||
t.Fatalf("expected IsRegistrationOpen to return false")
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ func runRepositoryContract(t *testing.T, backend contractBackend) {
|
||||
if err == nil {
|
||||
t.Error("expected an error for the duplicate email")
|
||||
}
|
||||
if all := repos.users.GetAll(); len(all) != 1 || all[0].Username != "first" {
|
||||
if all := allOf(repos.users.GetAll()); len(all) != 1 || all[0].Username != "first" {
|
||||
t.Errorf("nothing may be stored for the failed create, got %+v", all)
|
||||
}
|
||||
}},
|
||||
@@ -231,7 +231,7 @@ func runRepositoryContract(t *testing.T, backend contractBackend) {
|
||||
if !errors.Is(secondErr, domainInvite.ErrInviteNotFound) {
|
||||
t.Errorf("expected ErrInviteNotFound, got %v", secondErr)
|
||||
}
|
||||
if len(repos.users.GetAll()) != 1 {
|
||||
if len(allOf(repos.users.GetAll())) != 1 {
|
||||
t.Errorf("only the first accept may create a user")
|
||||
}
|
||||
}},
|
||||
@@ -244,7 +244,7 @@ func runRepositoryContract(t *testing.T, backend contractBackend) {
|
||||
if err == nil {
|
||||
t.Fatal("expected the taken username to be refused")
|
||||
}
|
||||
invites := repos.invites.GetAll()
|
||||
invites := allOf(repos.invites.GetAll())
|
||||
if len(invites) != 1 || invites[0].AcceptedAt != nil {
|
||||
t.Errorf("the invite must stay pending, got %+v", invites)
|
||||
}
|
||||
@@ -366,3 +366,10 @@ func TestSQLiteRepositoryContract(t *testing.T) {
|
||||
func TestMySQLRepositoryContract(t *testing.T) {
|
||||
runRepositoryContract(t, contractBackend{name: "mysql", open: openContractMySQL})
|
||||
}
|
||||
|
||||
func allOf[T any](all []T, err error) []T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return all
|
||||
}
|
||||
@@ -165,20 +165,20 @@ func TestArticleRepositoryWritesOnSQLite(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if all := repo.GetAll(); len(all) != 2 {
|
||||
if all := allOf(repo.GetAll()); len(all) != 2 {
|
||||
t.Errorf("articles = %+v", all)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetAll returns an empty list when the query fails", func(t *testing.T) {
|
||||
t.Run("GetAll returns the error when the query fails", func(t *testing.T) {
|
||||
repo, db, _ := setup(t)
|
||||
fault := injectDatabaseFault(db, "fault:getall", faultOnQuery)
|
||||
fault.armed.Store(true)
|
||||
|
||||
all := repo.GetAll()
|
||||
all, err := repo.GetAll()
|
||||
|
||||
if all == nil || len(all) != 0 {
|
||||
t.Errorf("articles = %#v", all)
|
||||
if err == nil || len(all) != 0 {
|
||||
t.Errorf("articles = %#v, err = %v", all, err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -436,7 +436,7 @@ func TestSiteDeletionIsCheckedInsideTheTransaction(t *testing.T) {
|
||||
_, updateErr := articles.Update(created.ID, article.Update{SiteID: &shop.ID})
|
||||
|
||||
unchanged, _ := articles.Get(created.ID)
|
||||
if !errors.Is(createErr, site.ErrSiteNotFound) || !errors.Is(updateErr, site.ErrSiteNotFound) || unchanged.SiteID != blog.ID || len(articles.GetAll()) != 1 {
|
||||
if !errors.Is(createErr, site.ErrSiteNotFound) || !errors.Is(updateErr, site.ErrSiteNotFound) || unchanged.SiteID != blog.ID || len(allOf(articles.GetAll())) != 1 {
|
||||
t.Errorf("create %v, update %v, site %d", createErr, updateErr, unchanged.SiteID)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -113,7 +113,7 @@ func TestUnverifiedUsersAndReregistrationRepository(t *testing.T) {
|
||||
t.Fatal("expected Create to fail because the username is still in use")
|
||||
}
|
||||
|
||||
users := repo.GetAll()
|
||||
users := allOf(repo.GetAll())
|
||||
count := 0
|
||||
for _, u := range users {
|
||||
if u.Username == "alice" {
|
||||
|
||||
@@ -549,7 +549,7 @@ func TestListUsersReturnsEveryUser(t *testing.T) {
|
||||
repo.users[2] = user.User{ID: 2}
|
||||
uc := useCases.NewListUsersUseCase(repo)
|
||||
|
||||
result := uc.ListUsers()
|
||||
result, _ := uc.ListUsers()
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 users, got %d", len(result))
|
||||
|
||||
+5
-2
@@ -115,13 +115,16 @@ func (m *mockUserRepo) GetByEmail(email string) (user.User, error) {
|
||||
return user.User{}, user.ErrUserNotFound
|
||||
}
|
||||
|
||||
func (m *mockUserRepo) GetAll() []user.User {
|
||||
func (m *mockUserRepo) GetAll() ([]user.User, error) {
|
||||
m.getAllCalls++
|
||||
if err := m.fail.trigger("GetAll"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]user.User, 0)
|
||||
for _, id := range m.sortedIds() {
|
||||
result = append(result, m.users[id])
|
||||
}
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *mockUserRepo) FindByIDs(ids []uint32) ([]user.User, error) {
|
||||
|
||||
@@ -260,7 +260,7 @@ type usersAPIGetAllHookRepo struct {
|
||||
hook func()
|
||||
}
|
||||
|
||||
func (r *usersAPIGetAllHookRepo) GetAll() []user.User {
|
||||
func (r *usersAPIGetAllHookRepo) GetAll() ([]user.User, error) {
|
||||
if r.hook != nil {
|
||||
hook := r.hook
|
||||
r.hook = nil
|
||||
|
||||
@@ -167,7 +167,7 @@ func TestUsersAPIInviteRepository(t *testing.T) {
|
||||
if err != nil || renewed.RoleID != writer {
|
||||
t.Fatalf("unexpected %+v, %v", renewed, err)
|
||||
}
|
||||
if all := repo.GetAll(); len(all) != 1 || all[0].RoleID != writer {
|
||||
if all := allOf(repo.GetAll()); len(all) != 1 || all[0].RoleID != writer {
|
||||
t.Errorf("unexpected %+v", all)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import ApiSearchPanel from '@/components/ApiSearchPanel.vue'
|
||||
import type { SearchEntry, SearchIndex } from '@/lib/apiSearch'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
defineProps<{
|
||||
index: SearchIndex
|
||||
@@ -12,10 +12,8 @@ defineProps<{
|
||||
|
||||
const emit = defineEmits<{ select: [entry: SearchEntry] }>()
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 64rem)'
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop('64rem')
|
||||
|
||||
const contentClass = computed(() =>
|
||||
isDesktop.value
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
import { t, formatNumber } from '@/i18n'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { pageItems, rangeFor } from '@/lib/pagination'
|
||||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const props = defineProps<{
|
||||
page: number
|
||||
@@ -23,7 +21,7 @@ const emit = defineEmits<{
|
||||
'update:size': [size: number]
|
||||
}>()
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
|
||||
const busy = computed(() => props.loadingPage !== null)
|
||||
const atStart = computed(() => busy.value || props.page <= 1)
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, ref, useId } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import AuthorAvatar from '@/components/AuthorAvatar.vue'
|
||||
import type { Byline } from '@/composables/useArticles'
|
||||
import { moveItem } from '@/lib/arrayMove'
|
||||
import { insertionTarget } from '@/lib/articleMeta'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 64rem)'
|
||||
const DESKTOP_AVATAR = 22
|
||||
const MOBILE_AVATAR = 28
|
||||
|
||||
@@ -19,7 +18,7 @@ const props = withDefaults(defineProps<{ authors: Byline[]; hintHidden?: boolean
|
||||
const emit = defineEmits<{ 'update:authors': [authors: Byline[]] }>()
|
||||
|
||||
const labelId = useId()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop('64rem')
|
||||
const list = ref<HTMLElement | null>(null)
|
||||
const working = ref<Byline[] | null>(null)
|
||||
const grabbed = ref<number | null>(null)
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import ColumnPickerList from '@/components/ColumnPickerList.vue'
|
||||
import type { BuiltinField } from '@/lib/contentTypes'
|
||||
import { COLUMN_CAP } from '@/lib/entryColumns'
|
||||
import { BOTTOM_SHEET_UI } from '@/lib/sheetUi'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const FOCUS_RING =
|
||||
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary'
|
||||
const TRIGGER = `h-11 w-11 justify-center rounded-none border border-accented bg-default p-0 text-highlighted ring-0 md:h-8 md:w-auto md:px-3 ${FOCUS_RING}`
|
||||
@@ -17,7 +16,7 @@ defineProps<{ fields: BuiltinField[]; keys: string[]; atCap: boolean; disabled?:
|
||||
|
||||
const emit = defineEmits<{ toggle: [key: string, on: boolean]; reset: [] }>()
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import ColumnPicker from '@/components/ColumnPicker.vue'
|
||||
import SortSelect from '@/components/SortSelect.vue'
|
||||
import type { BuiltinField } from '@/lib/contentTypes'
|
||||
import type { EntrySortField } from '@/lib/entryListQuery'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
defineProps<{
|
||||
sort: string
|
||||
@@ -25,7 +23,7 @@ const emit = defineEmits<{
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import DirectoryRowMenu from '@/components/DirectoryRowMenu.vue'
|
||||
import type { DirectoryMenuItem } from '@/lib/directoryMenu'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const DEFAULT_SKELETON_ROWS = 3
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -31,7 +30,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits<{ open: [item: T] }>()
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const openMenuId = ref<number | null>(null)
|
||||
|
||||
function setMenuOpen(item: T, isOpen: boolean) {
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, ref, useId } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useLocalizedOverlay } from '@/i18n/presentation'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import type { DirectoryNotice } from '@/lib/directoryTypes'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const AUTOFOCUS_SELECTOR = '[data-autofocus]'
|
||||
const INVALID_SELECTOR = '[aria-invalid="true"]'
|
||||
|
||||
@@ -38,7 +37,7 @@ const emit = defineEmits<{ close: []; submit: [] }>()
|
||||
const open = defineModel<boolean>('open', { default: true })
|
||||
|
||||
const overlay = useLocalizedOverlay()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const formId = useId()
|
||||
const form = ref<{ $el: HTMLElement } | null>(null)
|
||||
const side = computed(() => (isDesktop.value ? 'right' : 'bottom'))
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { t, formatNumber } from '@/i18n'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { useElementBounding, useMediaQuery } from '@vueuse/core'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const CONTENT_SELECTOR = '#dashboard-content'
|
||||
|
||||
defineProps<{
|
||||
@@ -18,7 +18,7 @@ const emit = defineEmits<{
|
||||
clear: []
|
||||
}>()
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const content = useElementBounding(() => document.querySelector<HTMLElement>(CONTENT_SELECTOR))
|
||||
const centre = computed(() => `${content.left.value + content.width.value / 2}px`)
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import MediaArticleStatusBadge from '@/components/MediaArticleStatusBadge.vue'
|
||||
import MediaThumb from '@/components/MediaThumb.vue'
|
||||
import MediaVariantsTab from '@/components/variants/MediaVariantsTab.vue'
|
||||
@@ -19,8 +18,8 @@ import { usageSplitOf } from '@/lib/mediaUsage'
|
||||
import { PANEL_TAB_TRIGGER } from '@/lib/tabStyles'
|
||||
import { useCanManageVariants } from '@/composables/useCanManageVariants'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const ACTION_BUTTON =
|
||||
'h-11 justify-center border border-accented bg-default px-3 text-sm ring-0 max-md:flex-1 md:h-8'
|
||||
|
||||
@@ -41,7 +40,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const canManageVariants = useCanManageVariants()
|
||||
const permissions = usePermissionsStore()
|
||||
const canRename = computed(() => permissions.can('media.upload'))
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, ref, useId } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import DirectoryField from '@/components/DirectoryField.vue'
|
||||
import ImageUploader from '@/components/ImageUploader.vue'
|
||||
import type { MediaItem } from '@/composables/useMedia'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
import {
|
||||
imageButtonName,
|
||||
imageMeta,
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type MediaSummary,
|
||||
} from '@/lib/mediaField'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const BUTTON_CLASS = 'h-11 gap-1.5 px-3 text-sm text-highlighted disabled:opacity-40 md:h-8'
|
||||
const OUTLINE_CLASS = `${BUTTON_CLASS} border border-accented bg-default ring-0`
|
||||
const ROW_BUTTON_CLASS = `${BUTTON_CLASS} max-md:flex-1`
|
||||
@@ -46,7 +45,7 @@ const labelId = `${id}-label`
|
||||
const helpId = `${id}-help`
|
||||
const errorId = `${id}-error`
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const pickerOpen = ref(false)
|
||||
const hovered = ref(false)
|
||||
const announcement = ref('')
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import MediaThumb from '@/components/MediaThumb.vue'
|
||||
import VariantPickerField from '@/components/variants/VariantPickerField.vue'
|
||||
import type { LibraryStatus } from '@/composables/useLibraryList'
|
||||
@@ -10,9 +9,9 @@ import type { ImageVariant } from '@/composables/useImageVariants'
|
||||
import type { MediaItem } from '@/composables/useMedia'
|
||||
import type { FieldPick } from '@/lib/imageChoice'
|
||||
import { accessibleName, formatDimensions, formatSize } from '@/lib/mediaFormat'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 120
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const FIRST_LOAD_SKELETONS_DESKTOP = 8
|
||||
const FIRST_LOAD_SKELETONS_MOBILE = 9
|
||||
const COLUMNS_DESKTOP = 4
|
||||
@@ -62,7 +61,7 @@ const tileAspect = computed(() =>
|
||||
const alt = defineModel<string>('alt', { default: '' })
|
||||
const variant = defineModel<string>('variant', { default: '' })
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const grid = ref<HTMLElement | null>(null)
|
||||
const searchText = ref(props.query)
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { t, formatNumber } from '@/i18n'
|
||||
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import MediaVariantsFilter from '@/components/MediaVariantsFilter.vue'
|
||||
import type { MediaCounts } from '@/composables/useMedia'
|
||||
import type {
|
||||
@@ -13,9 +12,9 @@ import type {
|
||||
MediaView,
|
||||
} from '@/lib/mediaQuery'
|
||||
import { UNDERLINE_TAB_TRIGGER } from '@/lib/tabStyles'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 120
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
|
||||
const SORT_LABELS: Record<MediaSort, string> = {
|
||||
get newest() {
|
||||
@@ -71,7 +70,7 @@ const emit = defineEmits<{
|
||||
purge: []
|
||||
}>()
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const searchText = ref(props.controls.q)
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import { useEventListener, useMediaQuery } from '@vueuse/core'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { MEDIA_USAGE_KEY } from '@/lib/mediaUsageContext'
|
||||
import { tooltipLines } from '@/lib/mediaUsage'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const TOOLTIP_UI = {
|
||||
content:
|
||||
'h-auto flex-col items-start gap-0 bg-inverted px-2 py-1.5 text-xs/4.5 text-inverted shadow-(--shadow-popover) ring-0',
|
||||
@@ -17,7 +17,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const usage = inject(MEDIA_USAGE_KEY, null)
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const hovered = ref(false)
|
||||
const dismissed = ref(false)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import ApiPreview from '@/components/ApiPreview.vue'
|
||||
import ApiPreviewSheet from '@/components/ApiPreviewSheet.vue'
|
||||
import BuilderActions from '@/components/BuilderActions.vue'
|
||||
@@ -22,12 +21,11 @@ import { useUnsavedChangesGuard } from '@/composables/useUnsavedChangesGuard'
|
||||
import { sampleJson } from '@/lib/schemaBuilderSample'
|
||||
import { entriesNotice } from '@/lib/schemaEntries'
|
||||
import { formatClock } from '@/lib/updateFormat'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const props = defineProps<{ edit?: EditSource }>()
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const builder = useSchemaBuilder(props.edit)
|
||||
const { form } = builder
|
||||
const remove = props.edit ? useSchemaDelete(() => props.edit!) : undefined
|
||||
|
||||
@@ -3,7 +3,7 @@ import AppSwitch from '@/components/AppSwitch.vue'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { createReusableTemplate, useMediaQuery } from '@vueuse/core'
|
||||
import { createReusableTemplate } from '@vueuse/core'
|
||||
import DirectoryField from '@/components/DirectoryField.vue'
|
||||
import DirectoryPanel from '@/components/DirectoryPanel.vue'
|
||||
import FallbackChip from '@/components/FallbackChip.vue'
|
||||
@@ -34,8 +34,8 @@ import {
|
||||
type SeoPreviewInput,
|
||||
} from '@/lib/seoPreview'
|
||||
import type { SeoFocus, SeoSuggestion, SuggestionCode, SuggestionField } from '@/lib/seoTypes'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const PANEL_FIELDS: SuggestionField[] = [
|
||||
'seo_title',
|
||||
'seo_description',
|
||||
@@ -79,7 +79,7 @@ const [DefineFields, Fields] = createReusableTemplate()
|
||||
const [DefinePreviews, Previews] = createReusableTemplate()
|
||||
const [DefinePreviewButton, PreviewButton] = createReusableTemplate()
|
||||
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const layout = ref<HTMLElement | null>(null)
|
||||
const focusTarget = ref<SeoFocus | null>(props.focus)
|
||||
const device = ref<PreviewDevice>('desktop')
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import { locale, t } from '@/i18n'
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { colorModeOptions, useAccountMenu } from '@/composables/useAccountMenu'
|
||||
import { chooseLanguage, languageOptions } from '@/composables/useLanguage'
|
||||
import { useAccountMenuRadios } from '@/composables/useRadioMenuRoles'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 64rem)'
|
||||
const ITEM_CLASS = 'h-8 items-center gap-2.5 px-2 py-0 text-sm font-normal text-highlighted'
|
||||
const CHOICE_CLASS = 'h-8 items-center gap-2.5 px-2 py-0 text-base font-normal text-highlighted'
|
||||
const SECTION_LABEL_CLASS =
|
||||
@@ -27,7 +26,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { colorMode, name, initials, onProfile, goToProfile } = useAccountMenu(() => props)
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop('64rem')
|
||||
const open = ref(false)
|
||||
useAccountMenuRadios(open)
|
||||
const chip = ref<{ $el: HTMLElement } | null>(null)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import ProfileCard from '@/components/ProfileCard.vue'
|
||||
import RoleBadge from '@/components/RoleBadge.vue'
|
||||
import RoleSummaryDialog from '@/components/RoleSummaryDialog.vue'
|
||||
import { buildHeldRolePreview } from '@/lib/rolePreview'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const permissions = usePermissionsStore()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const summaryOpen = ref(false)
|
||||
|
||||
const model = computed(() => buildHeldRolePreview(permissions.grants))
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useLocalizedOverlay } from '@/i18n/presentation'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import VariantFormFields from '@/components/variants/VariantFormFields.vue'
|
||||
@@ -19,8 +18,8 @@ import {
|
||||
type VariantFormValues,
|
||||
} from '@/lib/variants/variantForm'
|
||||
import { useImageVariantsStore } from '@/stores/imageVariants'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 40rem)'
|
||||
const CONFLICT = 409
|
||||
const UNPROCESSABLE = 422
|
||||
const TOAST_STATUSES = [400, 403, 404]
|
||||
@@ -39,7 +38,7 @@ const emit = defineEmits<{
|
||||
const store = useImageVariantsStore()
|
||||
const overlay = useLocalizedOverlay()
|
||||
const { announceFailure } = useVariantToasts()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop('40rem')
|
||||
const form = useVariantForm(props.mode, props.initial)
|
||||
const side = computed(() => (isDesktop.value ? 'right' : 'bottom'))
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ describe('getMe maps the account fields', () => {
|
||||
await expect(useApi().getMe()).resolves.toEqual({
|
||||
id: 1,
|
||||
verified: true,
|
||||
access: ownerAccess(),
|
||||
access: { ...ownerAccess(), roleAssignedBy: null },
|
||||
username: 'marta',
|
||||
email: 'marta@renew.example',
|
||||
displayName: 'Marta Alves',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useContentTypeDetail } from '@/composables/useContentTypeDetail'
|
||||
import { useEntryColumns } from '@/composables/useEntryColumns'
|
||||
import { useEntryList } from '@/composables/useEntryList'
|
||||
@@ -8,8 +7,8 @@ import { resolveContentListState } from '@/lib/contentListState'
|
||||
import { entryCaption } from '@/lib/entryListQuery'
|
||||
import { fieldCountLabel } from '@/lib/schemaRows'
|
||||
import { HOME_PATH, entryPath, schemaPath } from '@/lib/routePaths'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const DESKTOP_SKELETON_ROWS = 8
|
||||
const MOBILE_SKELETON_ROWS = 6
|
||||
|
||||
@@ -19,7 +18,7 @@ export function useContentListPage(
|
||||
canEdit: () => boolean,
|
||||
) {
|
||||
const router = useRouter()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const list = useEntryList(typeKey)
|
||||
const columns = useEntryColumns(typeKey, () => detail.type.value?.fields ?? [])
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { t } from '@/i18n'
|
||||
import { useLocalizedToast } from '@/i18n/presentation'
|
||||
import { useEntryDraft } from '@/composables/useEntryDraft'
|
||||
@@ -12,8 +11,8 @@ import { entryTitle } from '@/lib/entryForm'
|
||||
import type { EntryTarget } from '@/lib/entryEditorState'
|
||||
import { formatEntryStamp, formatSavedClock } from '@/lib/entryStamp'
|
||||
import { contentPath, entryPath } from '@/lib/routePaths'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const SAVED_TOAST_ID = 'entry-saved'
|
||||
const MISSING_NAME = '–'
|
||||
|
||||
@@ -26,7 +25,7 @@ export function useEntryEditorPage(
|
||||
) {
|
||||
const router = useRouter()
|
||||
const toast = useLocalizedToast()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const editor = useEntryDraft(typeKey, target)
|
||||
const { detail, load, form } = editor
|
||||
const names = useEntryNames(() => load.entry.value)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
|
||||
export type DesktopBreakpoint = '40rem' | '48rem' | '64rem'
|
||||
|
||||
export function useIsDesktop(minWidth: DesktopBreakpoint = '48rem') {
|
||||
return useMediaQuery(`(min-width: ${minWidth})`)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useApi } from '@/composables/useApi'
|
||||
|
||||
const JSON_HEADERS = { 'Content-Type': 'application/json' }
|
||||
|
||||
export function useProfile() {
|
||||
const { request } = useApi()
|
||||
|
||||
function updateDisplayName(displayName: string) {
|
||||
return request<{ display_name?: string | null }>('/v1/user/me', {
|
||||
method: 'PATCH',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({ display_name: displayName }),
|
||||
})
|
||||
}
|
||||
|
||||
function sendPasswordReset(email: string) {
|
||||
return request('/v1/auth/password-reset', {
|
||||
method: 'POST',
|
||||
headers: JSON_HEADERS,
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
}
|
||||
|
||||
return { updateDisplayName, sendPasswordReset }
|
||||
}
|
||||
@@ -42,7 +42,11 @@ beforeEach(() => {
|
||||
setViewportWidth(DESKTOP_WIDTH)
|
||||
setActivePinia(createPinia())
|
||||
useSessionStore().status = 'ready'
|
||||
request.mockReset().mockResolvedValue([])
|
||||
request
|
||||
.mockReset()
|
||||
.mockImplementation(async (path: string) =>
|
||||
path === '/v1/image-variants' ? { items: [] } : [],
|
||||
)
|
||||
vi.mocked(useApi).mockReturnValue({
|
||||
request,
|
||||
getMe: vi.fn().mockResolvedValue(null),
|
||||
@@ -50,10 +54,11 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.restoreAllMocks()
|
||||
wrapper?.unmount()
|
||||
wrapper = undefined
|
||||
await flushPromises()
|
||||
vi.unstubAllGlobals()
|
||||
vi.restoreAllMocks()
|
||||
document.body.innerHTML = ''
|
||||
localStorage.clear()
|
||||
initializeLocale()
|
||||
|
||||
@@ -3,7 +3,6 @@ import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import ActivityCardList from '@/components/ActivityCardList.vue'
|
||||
import ActivityChips from '@/components/ActivityChips.vue'
|
||||
import ActivityDetailsPanel from '@/components/ActivityDetailsPanel.vue'
|
||||
@@ -21,12 +20,11 @@ import { useActivityPage } from '@/composables/useActivityPage'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import type { ActivityFilters } from '@/lib/activity'
|
||||
import type { StripKey } from '@/lib/activitySummary'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const router = useRouter()
|
||||
const session = useSessionStore()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const activity = useActivityPage()
|
||||
const {
|
||||
filters,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useLocalizedOverlay, useLocalizedToast } from '@/i18n/presentation'
|
||||
import ApiKeyCardList from '@/components/ApiKeyCardList.vue'
|
||||
import ApiKeyReveal from '@/components/ApiKeyReveal.vue'
|
||||
@@ -16,8 +15,8 @@ import { toastsAboveBulkBar } from '@/composables/useToastOffset'
|
||||
import { displayPrefix } from '@/lib/apiKeys'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const AUTH_HANDLED_STATUSES = [401, 403]
|
||||
const NOT_FOUND_STATUS = 404
|
||||
const REVOKE_FAILED_COPY = "Couldn't revoke the key. Try again in a moment."
|
||||
@@ -30,7 +29,7 @@ const overlay = useLocalizedOverlay()
|
||||
const session = useSessionStore()
|
||||
const permissions = usePermissionsStore()
|
||||
const { listApiKeys, revokeApiKey } = useApiKeys()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
|
||||
const loadState = ref<LoadState>('loading')
|
||||
const keys = ref<ApiKey[]>([])
|
||||
|
||||
@@ -3,7 +3,6 @@ import { t, formatNumber } from '@/i18n'
|
||||
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useLocalizedOverlay } from '@/i18n/presentation'
|
||||
import AuthorAvatar from '@/components/AuthorAvatar.vue'
|
||||
import AuthorFormPanel from '@/components/AuthorFormPanel.vue'
|
||||
@@ -31,8 +30,8 @@ import { classifyFailure } from '@/lib/directoryErrors'
|
||||
import type { DirectoryMenuItem } from '@/lib/directoryMenu'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const LIST_COLUMNS = 'minmax(0,1.4fr) minmax(0,1fr) 110px 100px 44px'
|
||||
const INITIAL_SKELETON_ROWS = 6
|
||||
const NOBODY = 'none'
|
||||
@@ -49,7 +48,7 @@ const permissions = usePermissionsStore()
|
||||
const overlay = useLocalizedOverlay()
|
||||
const toasts = useDirectoryToasts()
|
||||
const { rememberOpener, restoreFocus, leavePanel, followEditLink } = useDirectoryView()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const { listAuthors, listAuthorPage, listAllAuthors, deleteAuthor } = useAuthors()
|
||||
const removal = useDirectoryDelete<Author>()
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { formatNumber, t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useLocalizedOverlay, useLocalizedToast } from '@/i18n/presentation'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import DashboardShell from '@/components/DashboardShell.vue'
|
||||
@@ -27,8 +26,8 @@ import { formatVariantSize } from '@/lib/variants/variantFormat'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useVariantCacheStore } from '@/stores/variantCache'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 40rem)'
|
||||
const SKELETON_DELAY_MS = 150
|
||||
const NOT_FOUND = 404
|
||||
const CONFIRM_WORD = 'purge'
|
||||
@@ -39,7 +38,7 @@ const store = useVariantCacheStore()
|
||||
const overlay = useLocalizedOverlay()
|
||||
const toast = useLocalizedToast()
|
||||
const { announceFailure, announceSuccess } = useVariantToasts()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop('40rem')
|
||||
|
||||
const showSkeleton = ref(false)
|
||||
const purgeUnusedButton = ref<{ $el: HTMLElement } | null>(null)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, onBeforeUnmount, provide, ref, watch } from 'vue'
|
||||
import { useRoute, type LocationQuery } from 'vue-router'
|
||||
import { useEventListener, useMediaQuery, useOnline } from '@vueuse/core'
|
||||
import { useEventListener, useOnline } from '@vueuse/core'
|
||||
import { useLocalizedToast } from '@/i18n/presentation'
|
||||
import AppPaginator from '@/components/AppPaginator.vue'
|
||||
import BulkDeleteMediaDialog from '@/components/BulkDeleteMediaDialog.vue'
|
||||
@@ -51,8 +51,8 @@ import { pageCountFor } from '@/lib/pagination'
|
||||
import { purgedToast } from '@/lib/mediaPurge'
|
||||
import { RATE_LIMITED_TEXT, RATE_LIMITED_TITLE, UPLOAD_ACCEPT } from '@/lib/mediaUploadRows'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const MEDIA_PATH = '/media'
|
||||
const TOAST_DURATION_MS = 5000
|
||||
const PERSISTENT_TOAST = 0
|
||||
@@ -84,7 +84,7 @@ const permissions = usePermissionsStore()
|
||||
const media = useMedia()
|
||||
const selection = useMediaSelection(media.items)
|
||||
const upload = useMediaUpload({ onAdded: refreshInBackground })
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const online = useOnline()
|
||||
|
||||
function differsOnlyByPage(query: MediaControls, current: MediaControls) {
|
||||
|
||||
@@ -10,7 +10,8 @@ import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import DashboardShell from '@/components/DashboardShell.vue'
|
||||
import ProfileCard from '@/components/ProfileCard.vue'
|
||||
import YourRoleCard from '@/components/YourRoleCard.vue'
|
||||
import { ApiError, authRedirectPending, useApi } from '@/composables/useApi'
|
||||
import { ApiError, authRedirectPending } from '@/composables/useApi'
|
||||
import { useProfile } from '@/composables/useProfile'
|
||||
import { useRetryAfter } from '@/composables/useRetryAfter'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useSessionStore, type Session } from '@/stores/session'
|
||||
@@ -36,7 +37,7 @@ const session = useSessionStore()
|
||||
const permissions = usePermissionsStore()
|
||||
const toast = useLocalizedToast()
|
||||
const overlay = useLocalizedOverlay()
|
||||
const { request } = useApi()
|
||||
const { updateDisplayName, sendPasswordReset } = useProfile()
|
||||
const retryAfter = useRetryAfter()
|
||||
|
||||
const account = ref<Session | null>(session.session)
|
||||
@@ -189,11 +190,7 @@ async function onSave() {
|
||||
saveFailed.value = false
|
||||
serverLimitError.value = false
|
||||
try {
|
||||
const updated = await request<{ display_name?: string | null }>('/v1/user/me', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ display_name: draft.value }),
|
||||
})
|
||||
const updated = await updateDisplayName(draft.value)
|
||||
const displayName = updated.display_name ?? ''
|
||||
session.setDisplayName(displayName)
|
||||
field.value = displayName
|
||||
@@ -223,11 +220,7 @@ async function onSendReset() {
|
||||
}
|
||||
resetState.value = 'sending'
|
||||
try {
|
||||
await request('/v1/auth/password-reset', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: email.value }),
|
||||
})
|
||||
await sendPasswordReset(email.value)
|
||||
resetState.value = 'sent'
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status === 429) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import DashboardShell from '@/components/DashboardShell.vue'
|
||||
import PermissionGrid from '@/components/PermissionGrid.vue'
|
||||
import PermissionGroup from '@/components/PermissionGroup.vue'
|
||||
@@ -14,12 +13,12 @@ import { useLocalizedOverlay } from '@/i18n/presentation'
|
||||
import { useRoleEditor } from '@/composables/useRoleEditor'
|
||||
import type { CallerScope } from '@/lib/grantLocks'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 64rem)'
|
||||
const CONTENT_GROUP = 'content'
|
||||
|
||||
const permissions = usePermissionsStore()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop('64rem')
|
||||
const editor = useRoleEditor()
|
||||
const overlay = useLocalizedOverlay()
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { t } from '@/i18n'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useLocalizedToast } from '@/i18n/presentation'
|
||||
import DashboardShell from '@/components/DashboardShell.vue'
|
||||
import DeleteRoleDialog from '@/components/DeleteRoleDialog.vue'
|
||||
@@ -14,14 +13,13 @@ import { useRoles } from '@/composables/useRoles'
|
||||
import { canCover } from '@/lib/grantLocks'
|
||||
import type { RoleItem } from '@/lib/roles'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useLocalizedToast()
|
||||
const permissions = usePermissionsStore()
|
||||
const { listRoles } = useRoles()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const skeleton = useDelayedSkeleton()
|
||||
|
||||
const loadState = ref<LoadState>('loading')
|
||||
|
||||
@@ -4,7 +4,6 @@ import { t } from '@/i18n'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import DashboardShell from '@/components/DashboardShell.vue'
|
||||
import DirectoryAddButton from '@/components/DirectoryAddButton.vue'
|
||||
import DirectoryLoadError from '@/components/DirectoryLoadError.vue'
|
||||
@@ -13,13 +12,13 @@ import NoAccessState from '@/components/NoAccessState.vue'
|
||||
import SchemaTable from '@/components/SchemaTable.vue'
|
||||
import { useSchemaGate } from '@/composables/useSchemaGate'
|
||||
import { builtinRow, customRow, schemaSummary } from '@/lib/schemaRows'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const NEW_TYPE_PATH = '/schema/new'
|
||||
|
||||
const router = useRouter()
|
||||
const permissions = usePermissionsStore()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
|
||||
const {
|
||||
sessionReady,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { t, formatNumber } from '@/i18n'
|
||||
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useLocalizedOverlay } from '@/i18n/presentation'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import DashboardShell from '@/components/DashboardShell.vue'
|
||||
@@ -29,8 +28,8 @@ import { classifyFailure } from '@/lib/directoryErrors'
|
||||
import type { DirectoryMenuItem } from '@/lib/directoryMenu'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
const LIST_COLUMNS = 'minmax(0,1.1fr) minmax(0,1.5fr) 110px 44px'
|
||||
const INITIAL_SKELETON_ROWS = 3
|
||||
|
||||
@@ -42,7 +41,7 @@ const permissions = usePermissionsStore()
|
||||
const overlay = useLocalizedOverlay()
|
||||
const toasts = useDirectoryToasts()
|
||||
const { rememberOpener, restoreFocus, leavePanel, followEditLink } = useDirectoryView()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
const { listAllSites, listSitePage, setDefaultSite, deleteSite } = useSites()
|
||||
const removal = useDirectoryDelete<Site>()
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import ChangeRoleDialog from '@/components/ChangeRoleDialog.vue'
|
||||
import DashboardShell from '@/components/DashboardShell.vue'
|
||||
import InviteUserModal from '@/components/InviteUserModal.vue'
|
||||
@@ -15,16 +14,16 @@ import type { AcceptedUser, UserListItem } from '@/lib/users'
|
||||
import type { UserMenuContext } from '@/lib/userMenu'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const SKELETON_DELAY_MS = 150
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
|
||||
type LoadState = 'loading' | 'ready' | 'error'
|
||||
|
||||
const session = useSessionStore()
|
||||
const permissions = usePermissionsStore()
|
||||
const { listUsers } = useUsers()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop()
|
||||
|
||||
const loadState = ref<LoadState>('loading')
|
||||
const items = ref<UserListItem[]>([])
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { formatNumber, t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import { useLocalizedOverlay } from '@/i18n/presentation'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import DashboardShell from '@/components/DashboardShell.vue'
|
||||
@@ -30,8 +29,8 @@ import { formatVariantSize } from '@/lib/variants/variantFormat'
|
||||
import { useImageVariantsStore } from '@/stores/imageVariants'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
import { useIsDesktop } from '@/composables/useIsDesktop'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 40rem)'
|
||||
const SKELETON_DELAY_MS = 150
|
||||
const NOT_FOUND = 404
|
||||
|
||||
@@ -46,7 +45,7 @@ const permissions = usePermissionsStore()
|
||||
const store = useImageVariantsStore()
|
||||
const overlay = useLocalizedOverlay()
|
||||
const { announceFailure, announceSuccess } = useVariantToasts()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const isDesktop = useIsDesktop('40rem')
|
||||
|
||||
const showSkeleton = ref(false)
|
||||
const formTarget = ref<FormTarget | null>(null)
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('Single relation, empty, focused search', () => {
|
||||
await typeSearch('venue', 'rue ')
|
||||
await settleSearch(vi)
|
||||
|
||||
expect(searches()).toEqual(['/v1/content-type/venues/entry?q=rue&page=1&size=10'])
|
||||
expect(searches()).toEqual(['/v1/content-type/venues/entry?q=rue&page=1&size=25'])
|
||||
expect(optionLabels()).toEqual([
|
||||
'Rue Verte hall Lyon · 120 seats',
|
||||
'Rue Verte yard Lyon · outdoor',
|
||||
@@ -103,7 +103,7 @@ describe('Single relation, empty, focused search', () => {
|
||||
|
||||
await openList('venue', vi)
|
||||
|
||||
expect(searches()).toEqual(['/v1/content-type/venues/entry?page=1&size=10'])
|
||||
expect(searches()).toEqual(['/v1/content-type/venues/entry?page=1&size=25'])
|
||||
expect(optionLabels()).toHaveLength(4)
|
||||
})
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('No match', () => {
|
||||
await flushPromises()
|
||||
await settleSearch(vi)
|
||||
expect(relationInput('venue').value).toBe('')
|
||||
expect(searches().at(-1)).toBe('/v1/content-type/venues/entry?page=1&size=10')
|
||||
expect(searches().at(-1)).toBe('/v1/content-type/venues/entry?page=1&size=25')
|
||||
})
|
||||
|
||||
it('says the type has no entries at all', async () => {
|
||||
@@ -123,7 +123,7 @@ describe('Search failures', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(calls.slice(before)).toEqual([
|
||||
'/v1/content-type/venues/entry?q=rue&page=1&size=10',
|
||||
'/v1/content-type/venues/entry?q=rue&page=1&size=25',
|
||||
])
|
||||
expect(optionLabels()).toHaveLength(3)
|
||||
},
|
||||
@@ -175,7 +175,7 @@ describe('Load more', () => {
|
||||
button('Load more')!.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(searches().at(-1)).toBe('/v1/content-type/venues/entry?page=2&size=10')
|
||||
expect(searches().at(-1)).toBe('/v1/content-type/venues/entry?page=2&size=25')
|
||||
expect(optionLabels()).toHaveLength(12)
|
||||
expect(button('Load more')).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -652,8 +652,7 @@ describe('Errors', () => {
|
||||
'The expiry must be a date in the future.',
|
||||
'You must be logged in to access this resource.',
|
||||
'This action needs a real session.',
|
||||
'Only owners and admins can do this.',
|
||||
'Only the owner can do this.',
|
||||
'missing_permission',
|
||||
'You must verify your account to access this resource.',
|
||||
'Article not found',
|
||||
'Image not found',
|
||||
@@ -697,8 +696,7 @@ describe('Errors table rows of the custom content types round', () => {
|
||||
|
||||
const order = [
|
||||
'This action needs a real session.',
|
||||
'Only owners and admins can do this.',
|
||||
'Only the owner can do this.',
|
||||
'missing_permission',
|
||||
'You must verify your account to access this resource.',
|
||||
].map((message) => text.indexOf(message))
|
||||
expect(order.every((position) => position > -1)).toBe(true)
|
||||
@@ -909,7 +907,7 @@ describe('Expanded cards of the custom content types round', () => {
|
||||
[
|
||||
'POST /v1/content-type/',
|
||||
['key', 'name', 'icon', 'fields'],
|
||||
['400', '401', 'Only owners and admins can do this.', 'content_type_key_taken'],
|
||||
['400', '401', '403', 'content_type_key_taken'],
|
||||
],
|
||||
[
|
||||
'PUT /v1/content-type/{key}',
|
||||
@@ -1134,7 +1132,7 @@ describe('Authentication copy of the custom content types round', () => {
|
||||
"Requests act as the user who created the key, with that user's role (owner or admin).",
|
||||
)
|
||||
expect(auth).toContain(
|
||||
'Owner and admin routes answer 403 to a key whose creator is neither.',
|
||||
'Routes that need a permission answer 403 "missing_permission" to a key when the role of its creator does not hold it.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -187,7 +187,7 @@ describe('Search and sharing group', () => {
|
||||
const wrapper = await mountIntegration()
|
||||
|
||||
const forbidden = ERROR_ROWS.find((row) => row.status === 403)!
|
||||
expect(forbidden.messages).toHaveLength(4)
|
||||
expect(forbidden.messages).toHaveLength(3)
|
||||
expect(forbidden.note).toBe(NOTE)
|
||||
const notes = wrapper.findAll('[data-testid="error-note"]').map((item) => item.text())
|
||||
expect(notes.filter((item) => item === NOTE)).toHaveLength(2)
|
||||
|
||||
Reference in new issue
Block a user