mirror of https://github.com/go-gitea/gitea.git
Merge branch 'main' into xormigrate
This commit is contained in:
commit
f0bdbfe5a8
|
@ -6,6 +6,7 @@ package git
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -21,11 +22,12 @@ import (
|
|||
|
||||
// LFSLock represents a git lfs lock of repository.
|
||||
type LFSLock struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||
Path string `xorm:"TEXT"`
|
||||
Created time.Time `xorm:"created"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
OwnerID int64 `xorm:"INDEX NOT NULL"`
|
||||
Owner *user_model.User `xorm:"-"`
|
||||
Path string `xorm:"TEXT"`
|
||||
Created time.Time `xorm:"created"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -37,6 +39,35 @@ func (l *LFSLock) BeforeInsert() {
|
|||
l.Path = util.PathJoinRel(l.Path)
|
||||
}
|
||||
|
||||
// LoadAttributes loads attributes of the lock.
|
||||
func (l *LFSLock) LoadAttributes(ctx context.Context) error {
|
||||
// Load owner
|
||||
if err := l.LoadOwner(ctx); err != nil {
|
||||
return fmt.Errorf("load owner: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOwner loads owner of the lock.
|
||||
func (l *LFSLock) LoadOwner(ctx context.Context) error {
|
||||
if l.Owner != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
owner, err := user_model.GetUserByID(ctx, l.OwnerID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
l.Owner = user_model.NewGhostUser()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
l.Owner = owner
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateLFSLock creates a new lock.
|
||||
func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) {
|
||||
dbCtx, committer, err := db.TxContext(ctx)
|
||||
|
@ -94,7 +125,7 @@ func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) {
|
|||
}
|
||||
|
||||
// GetLFSLockByRepoID returns a list of locks of repository.
|
||||
func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) ([]*LFSLock, error) {
|
||||
func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) (LFSLockList, error) {
|
||||
e := db.GetEngine(ctx)
|
||||
if page >= 0 && pageSize > 0 {
|
||||
start := 0
|
||||
|
@ -103,7 +134,7 @@ func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) (
|
|||
}
|
||||
e.Limit(pageSize, start)
|
||||
}
|
||||
lfsLocks := make([]*LFSLock, 0, pageSize)
|
||||
lfsLocks := make(LFSLockList, 0, pageSize)
|
||||
return lfsLocks, e.Find(&lfsLocks, &LFSLock{RepoID: repoID})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
)
|
||||
|
||||
// LFSLockList is a list of LFSLock
|
||||
type LFSLockList []*LFSLock
|
||||
|
||||
// LoadAttributes loads the attributes for the given locks
|
||||
func (locks LFSLockList) LoadAttributes(ctx context.Context) error {
|
||||
if len(locks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := locks.LoadOwner(ctx); err != nil {
|
||||
return fmt.Errorf("load owner: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOwner loads the owner of the locks
|
||||
func (locks LFSLockList) LoadOwner(ctx context.Context) error {
|
||||
if len(locks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
usersIDs := container.FilterSlice(locks, func(lock *LFSLock) (int64, bool) {
|
||||
return lock.OwnerID, true
|
||||
})
|
||||
users := make(map[int64]*user_model.User, len(usersIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
In("id", usersIDs).
|
||||
Find(&users); err != nil {
|
||||
return fmt.Errorf("find users: %w", err)
|
||||
}
|
||||
for _, v := range locks {
|
||||
v.Owner = users[v.OwnerID]
|
||||
if v.Owner == nil { // not exist
|
||||
v.Owner = user_model.NewGhostUser()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -117,3 +117,9 @@ func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Colum
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAllProjectIssueByIssueIDsAndProjectIDs delete all project's issues by issue's and project's ids
|
||||
func DeleteAllProjectIssueByIssueIDsAndProjectIDs(ctx context.Context, issueIDs, projectIDs []int64) error {
|
||||
_, err := db.GetEngine(ctx).In("project_id", projectIDs).In("issue_id", issueIDs).Delete(&ProjectIssue{})
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -296,6 +296,12 @@ func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, err
|
|||
return p, nil
|
||||
}
|
||||
|
||||
// GetAllProjectsIDsByOwnerID returns the all projects ids it owns
|
||||
func GetAllProjectsIDsByOwnerIDAndType(ctx context.Context, ownerID int64, projectType Type) ([]int64, error) {
|
||||
projects := make([]int64, 0)
|
||||
return projects, db.GetEngine(ctx).Table(&Project{}).Where("owner_id=? AND type=?", ownerID, projectType).Cols("id").Find(&projects)
|
||||
}
|
||||
|
||||
// UpdateProject updates project properties
|
||||
func UpdateProject(ctx context.Context, p *Project) error {
|
||||
if !IsCardTypeValid(p.CardType) {
|
||||
|
|
|
@ -466,6 +466,7 @@ name: Name
|
|||
title: Title
|
||||
about: About
|
||||
labels: ["label1", "label2"]
|
||||
assignees: ["user1", "user2"]
|
||||
ref: Ref
|
||||
body:
|
||||
- type: markdown
|
||||
|
@ -523,11 +524,12 @@ body:
|
|||
visible: [form]
|
||||
`,
|
||||
want: &api.IssueTemplate{
|
||||
Name: "Name",
|
||||
Title: "Title",
|
||||
About: "About",
|
||||
Labels: []string{"label1", "label2"},
|
||||
Ref: "Ref",
|
||||
Name: "Name",
|
||||
Title: "Title",
|
||||
About: "About",
|
||||
Labels: []string{"label1", "label2"},
|
||||
Assignees: []string{"user1", "user2"},
|
||||
Ref: "Ref",
|
||||
Fields: []*api.IssueFormField{
|
||||
{
|
||||
Type: "markdown",
|
||||
|
|
|
@ -177,19 +177,20 @@ const (
|
|||
// IssueTemplate represents an issue template for a repository
|
||||
// swagger:model
|
||||
type IssueTemplate struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Title string `json:"title" yaml:"title"`
|
||||
About string `json:"about" yaml:"about"` // Using "description" in a template file is compatible
|
||||
Labels IssueTemplateLabels `json:"labels" yaml:"labels"`
|
||||
Ref string `json:"ref" yaml:"ref"`
|
||||
Content string `json:"content" yaml:"-"`
|
||||
Fields []*IssueFormField `json:"body" yaml:"body"`
|
||||
FileName string `json:"file_name" yaml:"-"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Title string `json:"title" yaml:"title"`
|
||||
About string `json:"about" yaml:"about"` // Using "description" in a template file is compatible
|
||||
Labels IssueTemplateStringSlice `json:"labels" yaml:"labels"`
|
||||
Assignees IssueTemplateStringSlice `json:"assignees" yaml:"assignees"`
|
||||
Ref string `json:"ref" yaml:"ref"`
|
||||
Content string `json:"content" yaml:"-"`
|
||||
Fields []*IssueFormField `json:"body" yaml:"body"`
|
||||
FileName string `json:"file_name" yaml:"-"`
|
||||
}
|
||||
|
||||
type IssueTemplateLabels []string
|
||||
type IssueTemplateStringSlice []string
|
||||
|
||||
func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error {
|
||||
func (l *IssueTemplateStringSlice) UnmarshalYAML(value *yaml.Node) error {
|
||||
var labels []string
|
||||
if value.IsZero() {
|
||||
*l = labels
|
||||
|
@ -217,7 +218,7 @@ func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error {
|
|||
*l = labels
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateLabels", value.Line, value.ShortTag())
|
||||
return fmt.Errorf("line %d: cannot unmarshal %s into IssueTemplateStringSlice", value.Line, value.ShortTag())
|
||||
}
|
||||
|
||||
type IssueConfigContactLink struct {
|
||||
|
|
|
@ -42,7 +42,7 @@ func TestIssueTemplate_Type(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIssueTemplateLabels_UnmarshalYAML(t *testing.T) {
|
||||
func TestIssueTemplateStringSlice_UnmarshalYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
|
@ -88,7 +88,7 @@ labels:
|
|||
b: bb
|
||||
`,
|
||||
tmpl: &IssueTemplate{},
|
||||
wantErr: "line 3: cannot unmarshal !!map into IssueTemplateLabels",
|
||||
wantErr: "line 3: cannot unmarshal !!map into IssueTemplateStringSlice",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
---- Part 1: CMU/UCD copyright notice: (BSD like) -----
|
||||
|
||||
Copyright 1989, 1991, 1992 by Carnegie Mellon University
|
||||
|
||||
Derivative Work - 1996, 1998-2000 Copyright 1996, 1998-2000 The Regents of the University of California
|
||||
|
||||
All Rights Reserved
|
||||
|
||||
Permission to use, copy, modify and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of CMU and The Regents of the University of California not be used in advertising or publicity pertaining to distribution of the software without specific written permission.
|
||||
|
||||
CMU AND THE REGENTS OF THE UNIVERSITY OF CALIFORNIA DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL CMU OR THE REGENTS OF THE UNIVERSITY OF CALIFORNIA BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM THE LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---- Part 2: Networks Associates Technology, Inc copyright notice (BSD) -----
|
||||
|
||||
Copyright (c) 2001-2003, Networks Associates Technology, Inc All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the Networks Associates Technology, Inc nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---- Part 3: Cambridge Broadband Ltd. copyright notice (BSD) -----
|
||||
|
||||
Portions of this code are copyright (c) 2001-2003, Cambridge Broadband Ltd. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* The name of Cambridge Broadband Ltd. may not be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---- Part 4: Sun Microsystems, Inc. copyright notice (BSD) -----
|
||||
|
||||
Copyright © 2003 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, California 95054, U.S.A. All rights reserved.
|
||||
|
||||
Use is subject to license terms below.
|
||||
|
||||
This distribution may include materials developed by third parties.
|
||||
|
||||
Sun, Sun Microsystems, the Sun logo and Solaris are trademarks or registered trademarks of Sun Microsystems, Inc. in the U.S. and other countries.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the Sun Microsystems, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---- Part 5: Sparta, Inc copyright notice (BSD) -----
|
||||
|
||||
Copyright (c) 2003-2009, Sparta, Inc All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Sparta, Inc nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---- Part 6: Cisco/BUPTNIC copyright notice (BSD) -----
|
||||
|
||||
Copyright (c) 2004, Cisco, Inc and Information Network Center of Beijing University of Posts and Telecommunications. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Cisco, Inc, Beijing University of Posts and Telecommunications, nor the names of their contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---- Part 7: Fabasoft R&D Software GmbH & Co KG copyright notice (BSD) -----
|
||||
|
||||
Copyright (c) Fabasoft R&D Software GmbH & Co KG, 2003 oss@fabasoft.com Author: Bernhard Penz
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
* The name of Fabasoft R&D Software GmbH & Co KG or any of its subsidiaries, brand or product names may not be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---- Part 8: Apple Inc. copyright notice (BSD) -----
|
||||
|
||||
Copyright (c) 2007 Apple Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of Apple Inc. ("Apple") nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---- Part 9: ScienceLogic, LLC copyright notice (BSD) -----
|
||||
|
||||
Copyright (c) 2009, ScienceLogic, LLC All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of ScienceLogic, LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,11 @@
|
|||
If you modify this Program, or any covered work, by linking or
|
||||
combining it with runtime libraries of Erlang/OTP as released by
|
||||
Ericsson on https://www.erlang.org (or a modified version of these
|
||||
libraries), containing parts covered by the terms of the Erlang Public
|
||||
License (https://www.erlang.org/EPLICENSE), the licensors of this
|
||||
Program grant you additional permission to convey the resulting work
|
||||
without the need to license the runtime libraries of Erlang/OTP under
|
||||
the GNU Affero General Public License. Corresponding Source for a
|
||||
non-source form of such a combination shall include the source code
|
||||
for the parts of the runtime libraries of Erlang/OTP used as well as
|
||||
that of the covered work.
|
|
@ -2186,6 +2186,7 @@ settings.transfer_in_progress = There is currently an ongoing transfer. Please c
|
|||
settings.transfer_notices_1 = - You will lose access to the repository if you transfer it to an individual user.
|
||||
settings.transfer_notices_2 = - You will keep access to the repository if you transfer it to an organization that you (co-)own.
|
||||
settings.transfer_notices_3 = - If the repository is private and is transferred to an individual user, this action makes sure that the user does have at least read permission (and changes permissions if necessary).
|
||||
settings.transfer_notices_4 = - If the repository belongs to an organization, and you transfer it to another organization or individual, you will lose the links between the repository's issues and the organization's project board.
|
||||
settings.transfer_owner = New Owner
|
||||
settings.transfer_perform = Perform Transfer
|
||||
settings.transfer_started = This repository has been marked for transfer and awaits confirmation from "%s"
|
||||
|
@ -2466,6 +2467,18 @@ settings.thread_id = Thread ID
|
|||
settings.matrix.homeserver_url = Homeserver URL
|
||||
settings.matrix.room_id = Room ID
|
||||
settings.matrix.message_type = Message Type
|
||||
settings.visibility.private.button = Make Private
|
||||
settings.visibility.private.text = Changing the visibility to private will not only make the repo visible to only allowed members but may remove the relation between it and forks, watchers, and stars.
|
||||
settings.visibility.private.bullet_title = <strong>Changing the visibility to private will:</strong>
|
||||
settings.visibility.private.bullet_one = Make the repo visible to only allowed members.
|
||||
settings.visibility.private.bullet_two = May remove the relation between it and <strong>forks</strong>, <strong>watchers</strong>, and <strong>stars</strong>.
|
||||
settings.visibility.public.button = Make Public
|
||||
settings.visibility.public.text = Changing the visibility to public will make the repo visible to anyone.
|
||||
settings.visibility.public.bullet_title= <strong>Changing the visibility to public will:</strong>
|
||||
settings.visibility.public.bullet_one = Make the repo visible to anyone.
|
||||
settings.visibility.success = Repository visibility changed.
|
||||
settings.visibility.error = An error occurred while trying to change the repo visibility.
|
||||
settings.visibility.fork_error = Can't change the visibility of a forked repo.
|
||||
settings.archive.button = Archive Repo
|
||||
settings.archive.header = Archive This Repo
|
||||
settings.archive.text = Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any issues or pull requests.
|
||||
|
|
|
@ -2868,6 +2868,7 @@ dashboard.reinit_missing_repos=レコードが存在するが見当たらない
|
|||
dashboard.sync_external_users=外部ユーザーデータの同期
|
||||
dashboard.cleanup_hook_task_table=hook_taskテーブルのクリーンアップ
|
||||
dashboard.cleanup_packages=期限切れパッケージのクリーンアップ
|
||||
dashboard.cleanup_actions=期限切れのActionsリソースのクリーンアップ
|
||||
dashboard.server_uptime=サーバーの稼働時間
|
||||
dashboard.current_goroutine=現在のGoroutine数
|
||||
dashboard.current_memory_usage=現在のメモリ使用量
|
||||
|
@ -2897,9 +2898,15 @@ dashboard.total_gc_time=GC停止時間の合計
|
|||
dashboard.total_gc_pause=GC停止時間の合計
|
||||
dashboard.last_gc_pause=前回のGC停止時間
|
||||
dashboard.gc_times=GC実行回数
|
||||
dashboard.delete_old_actions=データベースから古い操作履歴をすべて削除
|
||||
dashboard.delete_old_actions.started=データベースからの古い操作履歴の削除を開始しました。
|
||||
dashboard.update_checker=更新チェック
|
||||
dashboard.delete_old_system_notices=データベースから古いシステム通知をすべて削除
|
||||
dashboard.gc_lfs=LFSメタオブジェクトのガベージコレクション
|
||||
dashboard.stop_zombie_tasks=Actionsゾンビタスクを停止
|
||||
dashboard.stop_endless_tasks=終わらないActionsタスクを停止
|
||||
dashboard.cancel_abandoned_jobs=放置されたままのActionsジョブをキャンセル
|
||||
dashboard.start_schedule_tasks=Actionsスケジュールタスクを開始
|
||||
dashboard.sync_branch.started=ブランチの同期を開始しました
|
||||
dashboard.sync_tag.started=タグの同期を開始しました
|
||||
dashboard.rebuild_issue_indexer=イシューインデクサーの再構築
|
||||
|
@ -2974,6 +2981,10 @@ emails.not_updated=メール設定の更新に失敗しました: %v
|
|||
emails.duplicate_active=メールアドレスは別のユーザーが既に使用中です。
|
||||
emails.change_email_header=メール設定の更新
|
||||
emails.change_email_text=このメールアドレスで更新してもよろしいですか?
|
||||
emails.delete=メールアドレスの削除
|
||||
emails.delete_desc=このメールアドレスを削除してよろしいですか?
|
||||
emails.deletion_success=メールアドレスを削除しました。
|
||||
emails.delete_primary_email_error=プライマリメールアドレスを削除することはできません。
|
||||
|
||||
orgs.org_manage_panel=組織の管理
|
||||
orgs.name=名称
|
||||
|
@ -3666,6 +3677,7 @@ runs.no_workflows.quick_start=Gitea Actions の始め方がわからない?
|
|||
runs.no_workflows.documentation=Gitea Actions の詳細については、<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を参照してください。
|
||||
runs.no_runs=ワークフローはまだ実行されていません。
|
||||
runs.empty_commit_message=(空のコミットメッセージ)
|
||||
runs.expire_log_message=ログは古すぎるため消去されています。
|
||||
|
||||
workflow.disable=ワークフローを無効にする
|
||||
workflow.disable_success=ワークフロー '%s' が無効になりました。
|
||||
|
@ -3692,6 +3704,7 @@ variables.update.failed=変数を更新できませんでした。
|
|||
variables.update.success=変数を更新しました。
|
||||
|
||||
[projects]
|
||||
deleted.display_name=削除されたプロジェクト
|
||||
type-1.display_name=個人プロジェクト
|
||||
type-2.display_name=リポジトリ プロジェクト
|
||||
type-3.display_name=組織プロジェクト
|
||||
|
|
|
@ -1475,6 +1475,7 @@ issues.remove_labels=removeu os rótulos %s %s
|
|||
issues.add_remove_labels=adicionou o(s) rótulo(s) %s e removeu %s %s
|
||||
issues.add_milestone_at=`adicionou esta questão à etapa <b>%s</b> %s`
|
||||
issues.add_project_at=`adicionou esta questão ao planeamento <b>%s</b> %s`
|
||||
issues.move_to_column_of_project=`isto foi movido para %s dentro de %s em %s`
|
||||
issues.change_milestone_at=`modificou a etapa de <b>%s</b> para <b>%s</b> %s`
|
||||
issues.change_project_at=`modificou o planeamento de <b>%s</b> para <b>%s</b> %s`
|
||||
issues.remove_milestone_at=`removeu esta questão da etapa <b>%s</b> %s`
|
||||
|
|
|
@ -108,6 +108,7 @@
|
|||
"stylelint-declaration-strict-value": "1.10.6",
|
||||
"stylelint-value-no-unknown-custom-properties": "6.0.1",
|
||||
"svgo": "3.3.2",
|
||||
"type-fest": "4.23.0",
|
||||
"updates": "16.3.7",
|
||||
"vite-string-plugin": "1.3.4",
|
||||
"vitest": "2.0.5"
|
||||
|
@ -7439,6 +7440,19 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globals/node_modules/type-fest": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globby": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||
|
@ -12287,13 +12301,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz",
|
||||
"integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
|
|
|
@ -107,6 +107,7 @@
|
|||
"stylelint-declaration-strict-value": "1.10.6",
|
||||
"stylelint-value-no-unknown-custom-properties": "6.0.1",
|
||||
"svgo": "3.3.2",
|
||||
"type-fest": "4.23.0",
|
||||
"updates": "16.3.7",
|
||||
"vite-string-plugin": "1.3.4",
|
||||
"vitest": "2.0.5"
|
||||
|
|
|
@ -939,12 +939,23 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
|
|||
}
|
||||
}
|
||||
}
|
||||
selectedAssigneeIDs := make([]int64, 0, len(template.Assignees))
|
||||
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
|
||||
if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, false); err == nil {
|
||||
for _, userID := range userIDs {
|
||||
selectedAssigneeIDs = append(selectedAssigneeIDs, userID)
|
||||
selectedAssigneeIDStrings = append(selectedAssigneeIDStrings, strconv.FormatInt(userID, 10))
|
||||
}
|
||||
}
|
||||
|
||||
if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
|
||||
template.Ref = git.BranchPrefix + template.Ref
|
||||
}
|
||||
ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
|
||||
ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
|
||||
ctx.Data["HasSelectedAssignee"] = len(selectedAssigneeIDs) > 0
|
||||
ctx.Data["assignee_ids"] = strings.Join(selectedAssigneeIDStrings, ",")
|
||||
ctx.Data["SelectedAssigneeIDs"] = selectedAssigneeIDs
|
||||
ctx.Data["Reference"] = template.Ref
|
||||
ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
|
||||
return true, templateErrs
|
||||
|
|
|
@ -95,6 +95,11 @@ func LFSLocks(ctx *context.Context) {
|
|||
ctx.ServerError("LFSLocks", err)
|
||||
return
|
||||
}
|
||||
if err := lfsLocks.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("LFSLocks", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["LFSLocks"] = lfsLocks
|
||||
|
||||
if len(lfsLocks) == 0 {
|
||||
|
|
|
@ -170,15 +170,7 @@ func SettingsPost(ctx *context.Context) {
|
|||
form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
|
||||
}
|
||||
|
||||
visibilityChanged := repo.IsPrivate != form.Private
|
||||
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
|
||||
if visibilityChanged && setting.Repository.ForcePrivate && !form.Private && !ctx.Doer.IsAdmin {
|
||||
ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
|
||||
return
|
||||
}
|
||||
|
||||
repo.IsPrivate = form.Private
|
||||
if err := repo_service.UpdateRepository(ctx, repo, visibilityChanged); err != nil {
|
||||
if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
|
||||
ctx.ServerError("UpdateRepository", err)
|
||||
return
|
||||
}
|
||||
|
@ -940,6 +932,39 @@ func SettingsPost(ctx *context.Context) {
|
|||
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
|
||||
|
||||
case "visibility":
|
||||
if repo.IsFork {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
|
||||
if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin {
|
||||
ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
|
||||
return
|
||||
}
|
||||
|
||||
if repo.IsPrivate {
|
||||
err = repo_service.MakeRepoPublic(ctx, repo)
|
||||
} else {
|
||||
err = repo_service.MakeRepoPrivate(ctx, repo)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("Tried to change the visibility of the repo: %s", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success"))
|
||||
|
||||
log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name)
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
|
||||
|
||||
default:
|
||||
ctx.NotFound("", nil)
|
||||
}
|
||||
|
|
|
@ -122,6 +122,31 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili
|
|||
return committer.Commit()
|
||||
}
|
||||
|
||||
func UpdateRepositoryVisibility(ctx context.Context, repo *repo_model.Repository, isPrivate bool) (err error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer committer.Close()
|
||||
|
||||
repo.IsPrivate = isPrivate
|
||||
|
||||
if err = repo_module.UpdateRepository(ctx, repo, true); err != nil {
|
||||
return fmt.Errorf("UpdateRepositoryVisibility: %w", err)
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error) {
|
||||
return UpdateRepositoryVisibility(ctx, repo, false)
|
||||
}
|
||||
|
||||
func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err error) {
|
||||
return UpdateRepositoryVisibility(ctx, repo, true)
|
||||
}
|
||||
|
||||
// LinkedRepository returns the linked repo if any
|
||||
func LinkedRepository(ctx context.Context, a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) {
|
||||
if a.IssueID != 0 {
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -177,6 +178,22 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
|
|||
}
|
||||
}
|
||||
|
||||
// Remove project's issues that belong to old organization's projects
|
||||
if oldOwner.IsOrganization() {
|
||||
projects, err := project_model.GetAllProjectsIDsByOwnerIDAndType(ctx, oldOwner.ID, project_model.TypeOrganization)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to find old org projects: %w", err)
|
||||
}
|
||||
issues, err := issues_model.GetIssueIDsByRepoID(ctx, repo.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to find repo's issues: %w", err)
|
||||
}
|
||||
err = project_model.DeleteAllProjectIssueByIssueIDsAndProjectIDs(ctx, issues, projects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to delete project's issues: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if newOwner.IsOrganization() {
|
||||
teams, err := organization.FindOrgTeams(ctx, newOwner.ID)
|
||||
if err != nil {
|
||||
|
|
|
@ -155,8 +155,8 @@
|
|||
</div>
|
||||
<div class="no-select item">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
|
||||
{{range .Assignees}}
|
||||
<a class="item muted" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
|
||||
<span class="octicon-check tw-invisible">{{svg "octicon-check"}}</span>
|
||||
<a class="{{if SliceUtils.Contains $.SelectedAssigneeIDs .ID}}checked{{end}} item muted" href="#" data-id="{{.ID}}" data-id-selector="#assignee_{{.ID}}">
|
||||
<span class="octicon-check {{if not (SliceUtils.Contains $.SelectedAssigneeIDs .ID)}}tw-invisible{{end}}">{{svg "octicon-check"}}</span>
|
||||
<span class="text">
|
||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2"}}{{template "repo/search_name" .}}
|
||||
</span>
|
||||
|
@ -165,12 +165,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="ui assignees list">
|
||||
<span class="no-select item {{if .HasSelectedLabel}}tw-hidden{{end}}">
|
||||
<span class="no-select item {{if .HasSelectedAssignee}}tw-hidden{{end}}">
|
||||
{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}
|
||||
</span>
|
||||
<div class="selected">
|
||||
{{range .Assignees}}
|
||||
<a class="item tw-p-1 muted tw-hidden" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
|
||||
<a class="item tw-p-1 muted {{if not (SliceUtils.Contains $.SelectedAssigneeIDs .ID)}}tw-hidden{{end}}" id="assignee_{{.ID}}" href="{{$.RepoLink}}/issues?assignee={{.ID}}">
|
||||
{{ctx.AvatarUtils.Avatar . 28 "tw-mr-2 tw-align-middle"}}{{.GetDisplayName}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
|
|
@ -30,9 +30,9 @@
|
|||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{$.Owner.HomeLink}}">
|
||||
{{ctx.AvatarUtils.Avatar $.Owner}}
|
||||
{{$.Owner.DisplayName}}
|
||||
<a href="{{$lock.Owner.HomeLink}}">
|
||||
{{ctx.AvatarUtils.Avatar $lock.Owner}}
|
||||
{{$lock.Owner.DisplayName}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{TimeSince .Created ctx.Locale}}</td>
|
||||
|
|
|
@ -23,20 +23,6 @@
|
|||
<label>{{ctx.Locale.Tr "repo.template_helper"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{if not .Repository.IsFork}}
|
||||
<div class="inline field">
|
||||
<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
|
||||
<div class="ui checkbox" {{if and (not .Repository.IsPrivate) (gt .Repository.NumStars 0)}}data-tooltip-content="{{ctx.Locale.Tr "repo.stars_remove_warning"}}"{{end}}>
|
||||
{{if .IsAdmin}}
|
||||
<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}>
|
||||
{{else}}
|
||||
<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}{{if and $.ForcePrivate .Repository.IsPrivate}} disabled{{end}}>
|
||||
{{if and .Repository.IsPrivate $.ForcePrivate}}<input type="hidden" name="private" value="{{.Repository.IsPrivate}}">{{end}}
|
||||
{{end}}
|
||||
<label>{{ctx.Locale.Tr "repo.visibility_helper"}} {{if .Repository.NumForks}}<span class="text red">{{ctx.Locale.Tr "repo.visibility_fork_helper"}}</span>{{end}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field {{if .Err_Description}}error{{end}}">
|
||||
<label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label>
|
||||
<textarea id="description" name="description" rows="2" maxlength="2048">{{.Repository.Description}}</textarea>
|
||||
|
@ -786,6 +772,27 @@
|
|||
</h4>
|
||||
<div class="ui attached error danger segment">
|
||||
<div class="flex-list">
|
||||
{{if not .Repository.IsFork}}
|
||||
<div class="flex-item tw-items-center">
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">{{ctx.Locale.Tr "repo.visibility"}}</div>
|
||||
{{if .Repository.IsPrivate}}
|
||||
<div class="flex-item-body">{{ctx.Locale.Tr "repo.settings.visibility.public.text"}}</div>
|
||||
{{else}}
|
||||
<div class="flex-item-body">{{ctx.Locale.Tr "repo.settings.visibility.private.text"}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
<button class="ui basic red show-modal button" data-modal="#visibility-repo-modal">
|
||||
{{if .Repository.IsPrivate}}
|
||||
{{ctx.Locale.Tr "repo.settings.visibility.public.button"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.settings.visibility.private.button"}}
|
||||
{{end}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Repository.IsMirror}}
|
||||
<div class="flex-item">
|
||||
<div class="flex-item-main">
|
||||
|
@ -950,7 +957,8 @@
|
|||
<div class="ui warning message">
|
||||
{{ctx.Locale.Tr "repo.settings.transfer_notices_1"}} <br>
|
||||
{{ctx.Locale.Tr "repo.settings.transfer_notices_2"}} <br>
|
||||
{{ctx.Locale.Tr "repo.settings.transfer_notices_3"}}
|
||||
{{ctx.Locale.Tr "repo.settings.transfer_notices_3"}} <br>
|
||||
{{ctx.Locale.Tr "repo.settings.transfer_notices_4"}}
|
||||
</div>
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
|
@ -1012,6 +1020,34 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{if not .Repository.IsFork}}
|
||||
<div class="ui g-modal-confirm modal" id="visibility-repo-modal">
|
||||
<div class="header">
|
||||
{{ctx.Locale.Tr "repo.visibility"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
{{if .Repository.IsPrivate}}
|
||||
<p>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_title"}}</p>
|
||||
<ul>
|
||||
<li>{{ctx.Locale.Tr "repo.settings.visibility.public.bullet_one"}}</li>
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_title"}}</p>
|
||||
<ul>
|
||||
<li>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_one"}}</li>
|
||||
<li>{{ctx.Locale.Tr "repo.settings.visibility.private.bullet_two"}}{{if .Repository.NumForks}}<span class="text red">{{ctx.Locale.Tr "repo.visibility_fork_helper"}}</span>{{end}}</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
</div>
|
||||
<form action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="action" value="visibility">
|
||||
<input type="hidden" name="repo_id" value="{{.Repository.ID}}">
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeWiki}}
|
||||
<div class="ui small modal" id="delete-wiki-modal">
|
||||
<div class="header">
|
||||
|
|
|
@ -22345,6 +22345,9 @@
|
|||
"type": "string",
|
||||
"x-go-name": "About"
|
||||
},
|
||||
"assignees": {
|
||||
"$ref": "#/definitions/IssueTemplateStringSlice"
|
||||
},
|
||||
"body": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -22361,7 +22364,7 @@
|
|||
"x-go-name": "FileName"
|
||||
},
|
||||
"labels": {
|
||||
"$ref": "#/definitions/IssueTemplateLabels"
|
||||
"$ref": "#/definitions/IssueTemplateStringSlice"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
@ -22378,7 +22381,7 @@
|
|||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"IssueTemplateLabels": {
|
||||
"IssueTemplateStringSlice": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
|
|
|
@ -4,12 +4,21 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// check that files stored in LFS render properly in the web UI
|
||||
|
@ -81,3 +90,56 @@ func TestLFSRender(t *testing.T) {
|
|||
assert.Contains(t, content, "Testing READMEs in LFS")
|
||||
})
|
||||
}
|
||||
|
||||
// TestLFSLockView tests the LFS lock view on settings page of repositories
|
||||
func TestLFSLockView(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // in org 3
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // own by org 3
|
||||
session := loginUser(t, user2.Name)
|
||||
|
||||
// create a lock
|
||||
lockPath := "test_lfs_lock_view.zip"
|
||||
lockID := ""
|
||||
{
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks", repo3.FullName()), map[string]string{"path": lockPath})
|
||||
req.Header.Set("Accept", lfs.AcceptHeader)
|
||||
req.Header.Set("Content-Type", lfs.MediaType)
|
||||
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||
lockResp := &api.LFSLockResponse{}
|
||||
DecodeJSON(t, resp, lockResp)
|
||||
lockID = lockResp.Lock.ID
|
||||
}
|
||||
defer func() {
|
||||
// release the lock
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/%s/unlock", repo3.FullName(), lockID), map[string]string{})
|
||||
req.Header.Set("Accept", lfs.AcceptHeader)
|
||||
req.Header.Set("Content-Type", lfs.MediaType)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
}()
|
||||
|
||||
t.Run("owner name", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// make sure the display names are different, or the test is meaningless
|
||||
require.NoError(t, repo3.LoadOwner(context.Background()))
|
||||
require.NotEqual(t, user2.DisplayName(), repo3.Owner.DisplayName())
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/settings/lfs/locks", repo3.FullName()))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body).doc
|
||||
|
||||
tr := doc.Find("table#lfs-files-locks-table tbody tr")
|
||||
require.Equal(t, 1, tr.Length())
|
||||
|
||||
td := tr.First().Find("td")
|
||||
require.Equal(t, 4, td.Length())
|
||||
|
||||
// path
|
||||
assert.Equal(t, lockPath, strings.TrimSpace(td.Eq(0).Text()))
|
||||
// owner name
|
||||
assert.Equal(t, user2.DisplayName(), strings.TrimSpace(td.Eq(1).Text()))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,6 +3,11 @@ declare module '*.svg' {
|
|||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.css' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare let __webpack_public_path__: string;
|
||||
|
||||
interface Window {
|
||||
|
@ -20,3 +25,7 @@ declare module 'htmx.org/dist/htmx.esm.js' {
|
|||
const value = await import('htmx.org');
|
||||
export default value;
|
||||
}
|
||||
|
||||
interface Element {
|
||||
_tippy: import('tippy.js').Instance;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,9 @@
|
|||
.card-attachment-images {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
overflow: scroll;
|
||||
cursor: default;
|
||||
scroll-snap-type: x mandatory;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -78,7 +80,10 @@
|
|||
display: inline-block;
|
||||
max-height: 50px;
|
||||
border-radius: var(--border-radius);
|
||||
text-align: left;
|
||||
scroll-snap-align: center;
|
||||
margin-right: 2px;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.card-attachment-images img:only-child {
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui.button:focus-visible {
|
||||
box-shadow: inset 0 0 0 2px currentcolor;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.ui.button {
|
||||
white-space: normal;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: start;
|
||||
align-items: stretch;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--color-secondary);
|
||||
|
|
|
@ -52,7 +52,7 @@ function addCopyLink(file) {
|
|||
copyLinkEl.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const success = await clippie(generateMarkdownLinkForAttachment(file));
|
||||
showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error);
|
||||
showTemporaryTooltip(e.target as Element, success ? i18n.copy_success : i18n.copy_error);
|
||||
});
|
||||
file.previewTemplate.append(copyLinkEl);
|
||||
}
|
||||
|
|
|
@ -196,7 +196,7 @@ async function initIssuePinSort() {
|
|||
|
||||
createSortable(pinDiv, {
|
||||
group: 'shared',
|
||||
onEnd: pinMoveEnd,
|
||||
onEnd: pinMoveEnd, // eslint-disable-line @typescript-eslint/no-misused-promises
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ async function initRepoProjectSortable() {
|
|||
handle: '.project-column-header',
|
||||
delayOnTouchOnly: true,
|
||||
delay: 500,
|
||||
onSort: async () => {
|
||||
onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises
|
||||
boardColumns = mainBoard.querySelectorAll('.project-column');
|
||||
|
||||
const columnSorting = {
|
||||
|
@ -84,8 +84,8 @@ async function initRepoProjectSortable() {
|
|||
const boardCardList = boardColumn.querySelectorAll('.cards')[0];
|
||||
createSortable(boardCardList, {
|
||||
group: 'shared',
|
||||
onAdd: moveIssue,
|
||||
onUpdate: moveIssue,
|
||||
onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
|
||||
onUpdate: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
|
||||
delayOnTouchOnly: true,
|
||||
delay: 500,
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@ export function initStopwatch() {
|
|||
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
|
||||
|
||||
createTippy(stopwatchEl, {
|
||||
content: stopwatchPopup.cloneNode(true),
|
||||
content: stopwatchPopup.cloneNode(true) as Element,
|
||||
placement: 'bottom-end',
|
||||
trigger: 'click',
|
||||
maxWidth: 'none',
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||
|
||||
type DirElement = HTMLInputElement | HTMLTextAreaElement;
|
||||
|
||||
// for performance considerations, it only uses performant syntax
|
||||
function attachDirAuto(el) {
|
||||
function attachDirAuto(el: DirElement) {
|
||||
if (el.type !== 'hidden' &&
|
||||
el.type !== 'checkbox' &&
|
||||
el.type !== 'radio' &&
|
||||
|
@ -18,10 +20,12 @@ export function initDirAuto() {
|
|||
const mutation = mutationList[i];
|
||||
const len = mutation.addedNodes.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const addedNode = mutation.addedNodes[i];
|
||||
const addedNode = mutation.addedNodes[i] as HTMLElement;
|
||||
if (!isDocumentFragmentOrElementNode(addedNode)) continue;
|
||||
if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode);
|
||||
const children = addedNode.querySelectorAll('input, textarea');
|
||||
if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') {
|
||||
attachDirAuto(addedNode as DirElement);
|
||||
}
|
||||
const children = addedNode.querySelectorAll<DirElement>('input, textarea');
|
||||
const len = children.length;
|
||||
for (let childIdx = 0; childIdx < len; childIdx++) {
|
||||
attachDirAuto(children[childIdx]);
|
||||
|
@ -30,7 +34,7 @@ export function initDirAuto() {
|
|||
}
|
||||
});
|
||||
|
||||
const docNodes = document.querySelectorAll('input, textarea');
|
||||
const docNodes = document.querySelectorAll<DirElement>('input, textarea');
|
||||
const len = docNodes.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
attachDirAuto(docNodes[i]);
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export async function createSortable(el, opts = {}) {
|
||||
import type {SortableOptions} from 'sortablejs';
|
||||
|
||||
export async function createSortable(el, opts: {handle?: string} & SortableOptions = {}) {
|
||||
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
|
||||
|
||||
return new Sortable(el, {
|
||||
|
@ -15,5 +17,5 @@ export async function createSortable(el, opts = {}) {
|
|||
opts.onUnchoose?.(e);
|
||||
},
|
||||
...opts,
|
||||
});
|
||||
} satisfies SortableOptions);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {reactive} from 'vue';
|
||||
import type {Reactive} from 'vue';
|
||||
|
||||
let diffTreeStoreReactive;
|
||||
let diffTreeStoreReactive: Reactive<Record<string, any>>;
|
||||
export function diffTreeStore() {
|
||||
if (!diffTreeStoreReactive) {
|
||||
diffTreeStoreReactive = reactive(window.config.pageData.diffFileInfo);
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
import tippy, {followCursor} from 'tippy.js';
|
||||
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||
import {formatDatetime} from '../utils/time.ts';
|
||||
import type {Content, Instance, Props} from 'tippy.js';
|
||||
|
||||
const visibleInstances = new Set();
|
||||
type TippyOpts = {
|
||||
role?: string,
|
||||
theme?: 'default' | 'tooltip' | 'menu' | 'box-with-header' | 'bare',
|
||||
} & Partial<Props>;
|
||||
|
||||
const visibleInstances = new Set<Instance>();
|
||||
const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
|
||||
|
||||
export function createTippy(target, opts = {}) {
|
||||
export function createTippy(target: Element, opts: TippyOpts = {}) {
|
||||
// the callback functions should be destructured from opts,
|
||||
// because we should use our own wrapper functions to handle them, do not let the user override them
|
||||
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
||||
|
||||
const instance = tippy(target, {
|
||||
const instance: Instance = tippy(target, {
|
||||
appendTo: document.body,
|
||||
animation: false,
|
||||
allowHTML: false,
|
||||
|
@ -18,15 +24,15 @@ export function createTippy(target, opts = {}) {
|
|||
interactiveBorder: 20,
|
||||
ignoreAttributes: true,
|
||||
maxWidth: 500, // increase over default 350px
|
||||
onHide: (instance) => {
|
||||
onHide: (instance: Instance) => {
|
||||
visibleInstances.delete(instance);
|
||||
return onHide?.(instance);
|
||||
},
|
||||
onDestroy: (instance) => {
|
||||
onDestroy: (instance: Instance) => {
|
||||
visibleInstances.delete(instance);
|
||||
return onDestroy?.(instance);
|
||||
},
|
||||
onShow: (instance) => {
|
||||
onShow: (instance: Instance) => {
|
||||
// hide other tooltip instances so only one tooltip shows at a time
|
||||
for (const visibleInstance of visibleInstances) {
|
||||
if (visibleInstance.props.role === 'tooltip') {
|
||||
|
@ -43,7 +49,7 @@ export function createTippy(target, opts = {}) {
|
|||
theme: theme || role || 'default',
|
||||
plugins: [followCursor],
|
||||
...other,
|
||||
});
|
||||
} satisfies Partial<Props>);
|
||||
|
||||
if (role === 'menu') {
|
||||
target.setAttribute('aria-haspopup', 'true');
|
||||
|
@ -58,12 +64,8 @@ export function createTippy(target, opts = {}) {
|
|||
* If the target element has no content, then no tooltip will be attached, and it returns null.
|
||||
*
|
||||
* Note: "tooltip" doesn't equal to "tippy". "tooltip" means a auto-popup content, it just uses tippy as the implementation.
|
||||
*
|
||||
* @param target {HTMLElement}
|
||||
* @param content {null|string}
|
||||
* @returns {null|tippy}
|
||||
*/
|
||||
function attachTooltip(target, content = null) {
|
||||
function attachTooltip(target: Element, content: Content = null) {
|
||||
switchTitleToTooltip(target);
|
||||
|
||||
content = content ?? target.getAttribute('data-tooltip-content');
|
||||
|
@ -84,7 +86,7 @@ function attachTooltip(target, content = null) {
|
|||
placement: target.getAttribute('data-tooltip-placement') || 'top-start',
|
||||
followCursor: target.getAttribute('data-tooltip-follow-cursor') || false,
|
||||
...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
|
||||
};
|
||||
} as TippyOpts;
|
||||
|
||||
if (!target._tippy) {
|
||||
createTippy(target, props);
|
||||
|
@ -94,7 +96,7 @@ function attachTooltip(target, content = null) {
|
|||
return target._tippy;
|
||||
}
|
||||
|
||||
function switchTitleToTooltip(target) {
|
||||
function switchTitleToTooltip(target: Element) {
|
||||
let title = target.getAttribute('title');
|
||||
if (title) {
|
||||
// apply custom formatting to relative-time's tooltips
|
||||
|
@ -118,16 +120,15 @@ function switchTitleToTooltip(target) {
|
|||
* According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event
|
||||
* Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
|
||||
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
|
||||
* @param e {Event}
|
||||
*/
|
||||
function lazyTooltipOnMouseHover(e) {
|
||||
function lazyTooltipOnMouseHover(e: MouseEvent) {
|
||||
e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
|
||||
attachTooltip(this);
|
||||
}
|
||||
|
||||
// Activate the tooltip for current element.
|
||||
// If the element has no aria-label, use the tooltip content as aria-label.
|
||||
function attachLazyTooltip(el) {
|
||||
function attachLazyTooltip(el: Element) {
|
||||
el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
|
||||
|
||||
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label
|
||||
|
@ -140,15 +141,15 @@ function attachLazyTooltip(el) {
|
|||
}
|
||||
|
||||
// Activate the tooltip for all children elements.
|
||||
function attachChildrenLazyTooltip(target) {
|
||||
for (const el of target.querySelectorAll('[data-tooltip-content]')) {
|
||||
function attachChildrenLazyTooltip(target: Element) {
|
||||
for (const el of target.querySelectorAll<Element>('[data-tooltip-content]')) {
|
||||
attachLazyTooltip(el);
|
||||
}
|
||||
}
|
||||
|
||||
export function initGlobalTooltips() {
|
||||
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
|
||||
const observerConnect = (observer) => observer.observe(document, {
|
||||
const observerConnect = (observer: MutationObserver) => observer.observe(document, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributeFilter: ['data-tooltip-content', 'title'],
|
||||
|
@ -159,7 +160,7 @@ export function initGlobalTooltips() {
|
|||
for (const mutation of [...mutationList, ...pending]) {
|
||||
if (mutation.type === 'childList') {
|
||||
// mainly for Vue components and AJAX rendered elements
|
||||
for (const el of mutation.addedNodes) {
|
||||
for (const el of mutation.addedNodes as NodeListOf<Element>) {
|
||||
if (!isDocumentFragmentOrElementNode(el)) continue;
|
||||
attachChildrenLazyTooltip(el);
|
||||
if (el.hasAttribute('data-tooltip-content')) {
|
||||
|
@ -167,7 +168,7 @@ export function initGlobalTooltips() {
|
|||
}
|
||||
}
|
||||
} else if (mutation.type === 'attributes') {
|
||||
attachTooltip(mutation.target);
|
||||
attachTooltip(mutation.target as Element);
|
||||
}
|
||||
}
|
||||
observerConnect(observer);
|
||||
|
@ -177,7 +178,7 @@ export function initGlobalTooltips() {
|
|||
attachChildrenLazyTooltip(document.documentElement);
|
||||
}
|
||||
|
||||
export function showTemporaryTooltip(target, content) {
|
||||
export function showTemporaryTooltip(target: Element, content: Content) {
|
||||
// if the target is inside a dropdown, don't show the tooltip because when the dropdown
|
||||
// closes, the tippy would be pushed unsightly to the top-left of the screen like seen
|
||||
// on the issue comment menu.
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import tinycolor from 'tinycolor2';
|
||||
import type {ColorInput} from 'tinycolor2';
|
||||
|
||||
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
|
||||
// Keep this in sync with modules/util/color.go
|
||||
function getRelativeLuminance(color) {
|
||||
function getRelativeLuminance(color: ColorInput) {
|
||||
const {r, g, b} = tinycolor(color).toRgb();
|
||||
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
|
||||
}
|
||||
|
||||
function useLightText(backgroundColor) {
|
||||
function useLightText(backgroundColor: ColorInput) {
|
||||
return getRelativeLuminance(backgroundColor) < 0.453;
|
||||
}
|
||||
|
||||
// Given a background color, returns a black or white foreground color that the highest
|
||||
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
||||
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
||||
export function contrastColor(backgroundColor) {
|
||||
export function contrastColor(backgroundColor: ColorInput) {
|
||||
return useLightText(backgroundColor) ? '#fff' : '#000';
|
||||
}
|
||||
|
||||
function resolveColors(obj) {
|
||||
function resolveColors(obj: Record<string, string>) {
|
||||
const styles = window.getComputedStyle(document.documentElement);
|
||||
const getColor = (name) => styles.getPropertyValue(name).trim();
|
||||
const getColor = (name: string) => styles.getPropertyValue(name).trim();
|
||||
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import {debounce} from 'throttle-debounce';
|
||||
import type {Promisable} from 'type-fest';
|
||||
import type $ from 'jquery';
|
||||
|
||||
function elementsCall(el, func, ...args) {
|
||||
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
|
||||
type ElementsCallback = (el: Element) => Promisable<any>;
|
||||
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
|
||||
type IterableElements = NodeListOf<Element> | Array<Element>;
|
||||
|
||||
function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) {
|
||||
if (typeof el === 'string' || el instanceof String) {
|
||||
el = document.querySelectorAll(el);
|
||||
el = document.querySelectorAll(el as string);
|
||||
}
|
||||
if (el instanceof Node) {
|
||||
func(el, ...args);
|
||||
} else if (el.length !== undefined) {
|
||||
// this works for: NodeList, HTMLCollection, Array, jQuery
|
||||
for (const e of el) {
|
||||
for (const e of (el as IterableElements)) {
|
||||
func(e, ...args);
|
||||
}
|
||||
} else {
|
||||
|
@ -17,10 +24,10 @@ function elementsCall(el, func, ...args) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param el string (selector), Node, NodeList, HTMLCollection, Array or jQuery
|
||||
* @param el Element
|
||||
* @param force force=true to show or force=false to hide, undefined to toggle
|
||||
*/
|
||||
function toggleShown(el, force) {
|
||||
function toggleShown(el: Element, force: boolean) {
|
||||
if (force === true) {
|
||||
el.classList.remove('tw-hidden');
|
||||
} else if (force === false) {
|
||||
|
@ -32,26 +39,26 @@ function toggleShown(el, force) {
|
|||
}
|
||||
}
|
||||
|
||||
export function showElem(el) {
|
||||
export function showElem(el: ElementArg) {
|
||||
elementsCall(el, toggleShown, true);
|
||||
}
|
||||
|
||||
export function hideElem(el) {
|
||||
export function hideElem(el: ElementArg) {
|
||||
elementsCall(el, toggleShown, false);
|
||||
}
|
||||
|
||||
export function toggleElem(el, force) {
|
||||
export function toggleElem(el: ElementArg, force?: boolean) {
|
||||
elementsCall(el, toggleShown, force);
|
||||
}
|
||||
|
||||
export function isElemHidden(el) {
|
||||
const res = [];
|
||||
export function isElemHidden(el: ElementArg) {
|
||||
const res: boolean[] = [];
|
||||
elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden')));
|
||||
if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`);
|
||||
return res[0];
|
||||
}
|
||||
|
||||
function applyElemsCallback(elems, fn) {
|
||||
function applyElemsCallback(elems: IterableElements, fn?: ElementsCallback) {
|
||||
if (fn) {
|
||||
for (const el of elems) {
|
||||
fn(el);
|
||||
|
@ -60,20 +67,22 @@ function applyElemsCallback(elems, fn) {
|
|||
return elems;
|
||||
}
|
||||
|
||||
export function queryElemSiblings(el, selector = '*', fn) {
|
||||
return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn);
|
||||
export function queryElemSiblings(el: Element, selector = '*', fn?: ElementsCallback) {
|
||||
return applyElemsCallback(Array.from(el.parentNode.children).filter((child: Element) => {
|
||||
return child !== el && child.matches(selector);
|
||||
}), fn);
|
||||
}
|
||||
|
||||
// it works like jQuery.children: only the direct children are selected
|
||||
export function queryElemChildren(parent, selector = '*', fn) {
|
||||
export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback) {
|
||||
return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
|
||||
}
|
||||
|
||||
export function queryElems(selector, fn) {
|
||||
export function queryElems(selector: string, fn?: ElementsCallback) {
|
||||
return applyElemsCallback(document.querySelectorAll(selector), fn);
|
||||
}
|
||||
|
||||
export function onDomReady(cb) {
|
||||
export function onDomReady(cb: () => Promisable<void>) {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', cb);
|
||||
} else {
|
||||
|
@ -83,7 +92,7 @@ export function onDomReady(cb) {
|
|||
|
||||
// checks whether an element is owned by the current document, and whether it is a document fragment or element node
|
||||
// if it is, it means it is a "normal" element managed by us, which can be modified safely.
|
||||
export function isDocumentFragmentOrElementNode(el) {
|
||||
export function isDocumentFragmentOrElementNode(el: Element) {
|
||||
try {
|
||||
return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
||||
} catch {
|
||||
|
@ -108,12 +117,15 @@ export function isDocumentFragmentOrElementNode(el) {
|
|||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
// ---------------------------------------------------------------------
|
||||
export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
|
||||
export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom = 0}: {viewportMarginBottom?: number} = {}) {
|
||||
let isUserResized = false;
|
||||
// lastStyleHeight and initialStyleHeight are CSS values like '100px'
|
||||
let lastMouseX, lastMouseY, lastStyleHeight, initialStyleHeight;
|
||||
let lastMouseX: number;
|
||||
let lastMouseY: number;
|
||||
let lastStyleHeight: string;
|
||||
let initialStyleHeight: string;
|
||||
|
||||
function onUserResize(event) {
|
||||
function onUserResize(event: MouseEvent) {
|
||||
if (isUserResized) return;
|
||||
if (lastMouseX !== event.clientX || lastMouseY !== event.clientY) {
|
||||
const newStyleHeight = textarea.style.height;
|
||||
|
@ -133,7 +145,7 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
|
|||
|
||||
while (el !== document.body && el !== null) {
|
||||
offsetTop += el.offsetTop || 0;
|
||||
el = el.offsetParent;
|
||||
el = el.offsetParent as HTMLTextAreaElement;
|
||||
}
|
||||
|
||||
const top = offsetTop - document.defaultView.scrollY;
|
||||
|
@ -213,14 +225,15 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
export function onInputDebounce(fn) {
|
||||
export function onInputDebounce(fn: () => Promisable<any>) {
|
||||
return debounce(300, fn);
|
||||
}
|
||||
|
||||
type LoadableElement = HTMLEmbedElement | HTMLIFrameElement | HTMLImageElement | HTMLScriptElement | HTMLTrackElement;
|
||||
|
||||
// Set the `src` attribute on an element and returns a promise that resolves once the element
|
||||
// has loaded or errored. Suitable for all elements mention in:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/load_event
|
||||
export function loadElem(el, src) {
|
||||
// has loaded or errored.
|
||||
export function loadElem(el: LoadableElement, src: string) {
|
||||
return new Promise((resolve) => {
|
||||
el.addEventListener('load', () => resolve(true), {once: true});
|
||||
el.addEventListener('error', () => resolve(false), {once: true});
|
||||
|
@ -256,14 +269,14 @@ export function initSubmitEventPolyfill() {
|
|||
* @param {HTMLElement} element The element to check.
|
||||
* @returns {boolean} True if the element is visible.
|
||||
*/
|
||||
export function isElemVisible(element) {
|
||||
export function isElemVisible(element: HTMLElement) {
|
||||
if (!element) return false;
|
||||
|
||||
return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||
}
|
||||
|
||||
// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this
|
||||
export function replaceTextareaSelection(textarea, text) {
|
||||
export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) {
|
||||
const before = textarea.value.slice(0, textarea.selectionStart ?? undefined);
|
||||
const after = textarea.value.slice(textarea.selectionEnd ?? undefined);
|
||||
let success = true;
|
||||
|
@ -287,13 +300,13 @@ export function replaceTextareaSelection(textarea, text) {
|
|||
}
|
||||
|
||||
// Warning: Do not enter any unsanitized variables here
|
||||
export function createElementFromHTML(htmlString) {
|
||||
export function createElementFromHTML(htmlString: string) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = htmlString.trim();
|
||||
return div.firstChild;
|
||||
return div.firstChild as Element;
|
||||
}
|
||||
|
||||
export function createElementFromAttrs(tagName, attrs) {
|
||||
export function createElementFromAttrs(tagName: string, attrs: Record<string, any>) {
|
||||
const el = document.createElement(tagName);
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
|
@ -307,7 +320,7 @@ export function createElementFromAttrs(tagName, attrs) {
|
|||
return el;
|
||||
}
|
||||
|
||||
export function animateOnce(el, animationClassName) {
|
||||
export function animateOnce(el: Element, animationClassName: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
el.addEventListener('animationend', function onAnimationEnd() {
|
||||
el.classList.remove(animationClassName);
|
||||
|
|
Loading…
Reference in New Issue