Add topic support (#3711)
* add topic models and unit tests * fix comments * fix comment * add the UI to show or add topics for a repo * show topics on repositories list * fix test * don't show manage topics link when no permission * use green basic as topic label * fix topic label color * remove trace content * remove debug function
This commit is contained in:
		
							parent
							
								
									1946ce2954
								
							
						
					
					
						commit
						bec69f702b
					
				
					 17 changed files with 487 additions and 2 deletions
				
			
		
							
								
								
									
										11
									
								
								models/fixtures/repo_topic.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								models/fixtures/repo_topic.yml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| - | ||||
|   repo_id: 1 | ||||
|   topic_id: 1 | ||||
| 
 | ||||
| - | ||||
|   repo_id: 1 | ||||
|   topic_id: 2 | ||||
| 
 | ||||
| - | ||||
|   repo_id: 1 | ||||
|   topic_id: 3 | ||||
							
								
								
									
										13
									
								
								models/fixtures/topic.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								models/fixtures/topic.yml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| - | ||||
|   id: 1 | ||||
|   name: golang | ||||
|   repo_count: 1 | ||||
| 
 | ||||
| - | ||||
|   id: 2 | ||||
|   name: database | ||||
|   repo_count: 1 | ||||
| 
 | ||||
| - id: 3 | ||||
|   name: SQL | ||||
|   repo_count: 1 | ||||
|  | @ -199,6 +199,7 @@ type Repository struct { | |||
| 	Size          int64              `xorm:"NOT NULL DEFAULT 0"` | ||||
| 	IndexerStatus *RepoIndexerStatus `xorm:"-"` | ||||
| 	IsFsckEnabled bool               `xorm:"NOT NULL DEFAULT true"` | ||||
| 	Topics        []string           `xorm:"TEXT JSON"` | ||||
| 
 | ||||
| 	CreatedUnix util.TimeStamp `xorm:"INDEX created"` | ||||
| 	UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | ||||
|  |  | |||
							
								
								
									
										192
									
								
								models/topic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								models/topic.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,192 @@ | |||
| // Copyright 2018 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
| 	"github.com/go-xorm/builder" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	tables = append(tables, | ||||
| 		new(Topic), | ||||
| 		new(RepoTopic), | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| // Topic represents a topic of repositories
 | ||||
| type Topic struct { | ||||
| 	ID          int64 | ||||
| 	Name        string `xorm:"unique"` | ||||
| 	RepoCount   int | ||||
| 	CreatedUnix util.TimeStamp `xorm:"INDEX created"` | ||||
| 	UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | ||||
| } | ||||
| 
 | ||||
| // RepoTopic represents associated repositories and topics
 | ||||
| type RepoTopic struct { | ||||
| 	RepoID  int64 `xorm:"unique(s)"` | ||||
| 	TopicID int64 `xorm:"unique(s)"` | ||||
| } | ||||
| 
 | ||||
| // ErrTopicNotExist represents an error that a topic is not exist
 | ||||
| type ErrTopicNotExist struct { | ||||
| 	Name string | ||||
| } | ||||
| 
 | ||||
| // IsErrTopicNotExist checks if an error is an ErrTopicNotExist.
 | ||||
| func IsErrTopicNotExist(err error) bool { | ||||
| 	_, ok := err.(ErrTopicNotExist) | ||||
| 	return ok | ||||
| } | ||||
| 
 | ||||
| // Error implements error interface
 | ||||
| func (err ErrTopicNotExist) Error() string { | ||||
| 	return fmt.Sprintf("topic is not exist [name: %s]", err.Name) | ||||
| } | ||||
| 
 | ||||
| // GetTopicByName retrieves topic by name
 | ||||
| func GetTopicByName(name string) (*Topic, error) { | ||||
| 	var topic Topic | ||||
| 	if has, err := x.Where("name = ?", name).Get(&topic); err != nil { | ||||
| 		return nil, err | ||||
| 	} else if !has { | ||||
| 		return nil, ErrTopicNotExist{name} | ||||
| 	} | ||||
| 	return &topic, nil | ||||
| } | ||||
| 
 | ||||
| // FindTopicOptions represents the options when fdin topics
 | ||||
| type FindTopicOptions struct { | ||||
| 	RepoID  int64 | ||||
| 	Keyword string | ||||
| 	Limit   int | ||||
| 	Page    int | ||||
| } | ||||
| 
 | ||||
| func (opts *FindTopicOptions) toConds() builder.Cond { | ||||
| 	var cond = builder.NewCond() | ||||
| 	if opts.RepoID > 0 { | ||||
| 		cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID}) | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.Keyword != "" { | ||||
| 		cond = cond.And(builder.Like{"topic.name", opts.Keyword}) | ||||
| 	} | ||||
| 
 | ||||
| 	return cond | ||||
| } | ||||
| 
 | ||||
| // FindTopics retrieves the topics via FindTopicOptions
 | ||||
| func FindTopics(opts *FindTopicOptions) (topics []*Topic, err error) { | ||||
| 	sess := x.Select("topic.*").Where(opts.toConds()) | ||||
| 	if opts.RepoID > 0 { | ||||
| 		sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") | ||||
| 	} | ||||
| 	if opts.Limit > 0 { | ||||
| 		sess.Limit(opts.Limit, opts.Page*opts.Limit) | ||||
| 	} | ||||
| 	return topics, sess.Desc("topic.repo_count").Find(&topics) | ||||
| } | ||||
| 
 | ||||
| // SaveTopics save topics to a repository
 | ||||
| func SaveTopics(repoID int64, topicNames ...string) error { | ||||
| 	topics, err := FindTopics(&FindTopicOptions{ | ||||
| 		RepoID: repoID, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	sess := x.NewSession() | ||||
| 	defer sess.Close() | ||||
| 
 | ||||
| 	if err := sess.Begin(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	var addedTopicNames []string | ||||
| 	for _, topicName := range topicNames { | ||||
| 		if strings.TrimSpace(topicName) == "" { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		var found bool | ||||
| 		for _, t := range topics { | ||||
| 			if strings.EqualFold(topicName, t.Name) { | ||||
| 				found = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !found { | ||||
| 			addedTopicNames = append(addedTopicNames, topicName) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var removeTopics []*Topic | ||||
| 	for _, t := range topics { | ||||
| 		var found bool | ||||
| 		for _, topicName := range topicNames { | ||||
| 			if strings.EqualFold(topicName, t.Name) { | ||||
| 				found = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !found { | ||||
| 			removeTopics = append(removeTopics, t) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, topicName := range addedTopicNames { | ||||
| 		var topic Topic | ||||
| 		if has, err := sess.Where("name = ?", topicName).Get(&topic); err != nil { | ||||
| 			return err | ||||
| 		} else if !has { | ||||
| 			topic.Name = topicName | ||||
| 			topic.RepoCount = 1 | ||||
| 			if _, err := sess.Insert(&topic); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} else { | ||||
| 			topic.RepoCount++ | ||||
| 			if _, err := sess.ID(topic.ID).Cols("repo_count").Update(&topic); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if _, err := sess.Insert(&RepoTopic{ | ||||
| 			RepoID:  repoID, | ||||
| 			TopicID: topic.ID, | ||||
| 		}); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, topic := range removeTopics { | ||||
| 		topic.RepoCount-- | ||||
| 		if _, err := sess.ID(topic.ID).Cols("repo_count").Update(topic); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		if _, err := sess.Delete(&RepoTopic{ | ||||
| 			RepoID:  repoID, | ||||
| 			TopicID: topic.ID, | ||||
| 		}); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{ | ||||
| 		Topics: topicNames, | ||||
| 	}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return sess.Commit() | ||||
| } | ||||
							
								
								
									
										57
									
								
								models/topic_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								models/topic_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | |||
| // Copyright 2018 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package models | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestAddTopic(t *testing.T) { | ||||
| 	assert.NoError(t, PrepareTestDatabase()) | ||||
| 
 | ||||
| 	topics, err := FindTopics(&FindTopicOptions{}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 3, len(topics)) | ||||
| 
 | ||||
| 	topics, err = FindTopics(&FindTopicOptions{ | ||||
| 		Limit: 2, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 2, len(topics)) | ||||
| 
 | ||||
| 	topics, err = FindTopics(&FindTopicOptions{ | ||||
| 		RepoID: 1, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 3, len(topics)) | ||||
| 
 | ||||
| 	assert.NoError(t, SaveTopics(2, "golang")) | ||||
| 	topics, err = FindTopics(&FindTopicOptions{}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 3, len(topics)) | ||||
| 
 | ||||
| 	topics, err = FindTopics(&FindTopicOptions{ | ||||
| 		RepoID: 2, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 1, len(topics)) | ||||
| 
 | ||||
| 	assert.NoError(t, SaveTopics(2, "golang", "gitea")) | ||||
| 	topic, err := GetTopicByName("gitea") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 1, topic.RepoCount) | ||||
| 
 | ||||
| 	topics, err = FindTopics(&FindTopicOptions{}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 4, len(topics)) | ||||
| 
 | ||||
| 	topics, err = FindTopics(&FindTopicOptions{ | ||||
| 		RepoID: 2, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 2, len(topics)) | ||||
| } | ||||
|  | @ -516,3 +516,8 @@ type AddTimeManuallyForm struct { | |||
| func (f *AddTimeManuallyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | ||||
| 	return validate(errs, ctx.Data, f, ctx.Locale) | ||||
| } | ||||
| 
 | ||||
| // SaveTopicForm form for save topics for repository
 | ||||
| type SaveTopicForm struct { | ||||
| 	Topics []string `binding:"topics;Required;"` | ||||
| } | ||||
|  |  | |||
|  | @ -1114,6 +1114,9 @@ branch.restore_success = %s successfully restored | |||
| branch.restore_failed = Failed to restore branch %s. | ||||
| branch.protected_deletion_failed = It's not possible to delete protected branch %s. | ||||
| 
 | ||||
| topic.manage_topics = Manage Topics | ||||
| topic.done = Done | ||||
| 
 | ||||
| [org] | ||||
| org_name_holder = Organization Name | ||||
| org_full_name_holder = Organization Full Name | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -1591,6 +1591,7 @@ $(document).ready(function () { | |||
|     initTeamSettings(); | ||||
|     initCtrlEnterSubmit(); | ||||
|     initNavbarContentToggle(); | ||||
|     initTopicbar(); | ||||
| 
 | ||||
|     // Repo clone url.
 | ||||
|     if ($('#repo-clone-url').length > 0) { | ||||
|  | @ -2122,3 +2123,74 @@ function initNavbarContentToggle() { | |||
|         } | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| function initTopicbar() { | ||||
|     var mgrBtn = $("#manage_topic") | ||||
|     var editDiv = $("#topic_edit") | ||||
|     var viewDiv = $("#repo-topic") | ||||
|     var saveBtn = $("#save_topic") | ||||
| 
 | ||||
|     mgrBtn.click(function() { | ||||
|         viewDiv.hide(); | ||||
|         editDiv.show(); | ||||
|     }) | ||||
| 
 | ||||
|     saveBtn.click(function() { | ||||
|         var topics = $("input[name=topics]").val(); | ||||
| 
 | ||||
|         $.post($(this).data('link'), { | ||||
|             "_csrf": csrf, | ||||
|             "topics": topics | ||||
|         }).success(function(res){ | ||||
|             if (res["status"] != "ok") { | ||||
|                 alert(res.message); | ||||
|             } else { | ||||
|                 viewDiv.children(".topic").remove(); | ||||
|                 var topicArray = topics.split(","); | ||||
|                 var last = viewDiv.children("a").last(); | ||||
|                 for (var i=0;i < topicArray.length; i++) { | ||||
|                     $('<div class="ui green basic label topic" style="cursor:pointer;">'+topicArray[i]+'</div>').insertBefore(last) | ||||
|                 } | ||||
|             } | ||||
|         }).done(function() { | ||||
|             editDiv.hide(); | ||||
|             viewDiv.show(); | ||||
|         }) | ||||
|     }) | ||||
| 
 | ||||
|     $('#topic_edit .dropdown').dropdown({ | ||||
|         allowAdditions: true, | ||||
|         fields: { name: "description", value: "data-value" }, | ||||
|         saveRemoteData: false, | ||||
|         label: { | ||||
|             transition : 'horizontal flip', | ||||
|             duration   : 200, | ||||
|             variation  : false, | ||||
|             blue : true, | ||||
|             basic: true, | ||||
|         }, | ||||
|         className: { | ||||
|             label: 'ui green basic label' | ||||
|         }, | ||||
|         apiSettings: { | ||||
|             url: suburl + '/api/v1/topics/search?q={query}', | ||||
|             throttle: 500, | ||||
|             cache: false, | ||||
|             onResponse: function(res) { | ||||
|                 var formattedResponse = { | ||||
|                     success: false, | ||||
|                     results: new Array(), | ||||
|                 }; | ||||
| 
 | ||||
|                 if (res.topics) { | ||||
|                     formattedResponse.success = true; | ||||
|                     for (var i=0;i < res.topics.length;i++) { | ||||
|                         formattedResponse.results.push({"description": res.topics[i].Name, "data-value":res.topics[i].Name}) | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 return formattedResponse; | ||||
|             }, | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  | @ -1733,3 +1733,12 @@ tbody.commit-list { | |||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #topic_edit { | ||||
|     margin-top:5px; | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| #repo-topic { | ||||
|     margin-top: 5px; | ||||
| } | ||||
|  | @ -571,5 +571,9 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| 				}) | ||||
| 			}) | ||||
| 		}, reqAdmin()) | ||||
| 
 | ||||
| 		m.Group("/topics", func() { | ||||
| 			m.Get("/search", repo.TopicSearch) | ||||
| 		}) | ||||
| 	}, context.APIContexter()) | ||||
| } | ||||
|  |  | |||
|  | @ -501,3 +501,45 @@ func MirrorSync(ctx *context.APIContext) { | |||
| 	go models.MirrorQueue.Add(repo.ID) | ||||
| 	ctx.Status(200) | ||||
| } | ||||
| 
 | ||||
| // TopicSearch search for creating topic
 | ||||
| func TopicSearch(ctx *context.Context) { | ||||
| 	// swagger:operation GET /topics/search repository topicSearch
 | ||||
| 	// ---
 | ||||
| 	// summary: search topics via keyword
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: keyword
 | ||||
| 	//   in: path
 | ||||
| 	//   description: id of the repo to get
 | ||||
| 	//   type: integer
 | ||||
| 	//   required: true
 | ||||
| 	// responses:
 | ||||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/Repository"
 | ||||
| 	if ctx.User == nil { | ||||
| 		ctx.JSON(403, map[string]interface{}{ | ||||
| 			"message": "Only owners could change the topics.", | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	kw := ctx.Query("q") | ||||
| 
 | ||||
| 	topics, err := models.FindTopics(&models.FindTopicOptions{ | ||||
| 		Keyword: kw, | ||||
| 		Limit:   10, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Error(2, "SearchTopics failed: %v", err) | ||||
| 		ctx.JSON(500, map[string]interface{}{ | ||||
| 			"message": "Search topics failed.", | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSON(200, map[string]interface{}{ | ||||
| 		"topics": topics, | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										38
									
								
								routers/repo/topic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								routers/repo/topic.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | |||
| // Copyright 2018 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| ) | ||||
| 
 | ||||
| // TopicPost response for creating repository
 | ||||
| func TopicPost(ctx *context.Context) { | ||||
| 	if ctx.User == nil { | ||||
| 		ctx.JSON(403, map[string]interface{}{ | ||||
| 			"message": "Only owners could change the topics.", | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	topics := strings.Split(ctx.Query("topics"), ",") | ||||
| 
 | ||||
| 	err := models.SaveTopics(ctx.Repo.Repository.ID, topics...) | ||||
| 	if err != nil { | ||||
| 		log.Error(2, "SaveTopics failed: %v", err) | ||||
| 		ctx.JSON(500, map[string]interface{}{ | ||||
| 			"message": "Save topics failed.", | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSON(200, map[string]interface{}{ | ||||
| 		"status": "ok", | ||||
| 	}) | ||||
| } | ||||
|  | @ -314,6 +314,16 @@ func renderCode(ctx *context.Context) { | |||
| 		treeLink += "/" + ctx.Repo.TreePath | ||||
| 	} | ||||
| 
 | ||||
| 	// Get Topics of this repo
 | ||||
| 	topics, err := models.FindTopics(&models.FindTopicOptions{ | ||||
| 		RepoID: ctx.Repo.Repository.ID, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("models.FindTopics", err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Data["Topics"] = topics | ||||
| 
 | ||||
| 	// Get current entry user currently looking at.
 | ||||
| 	entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -587,6 +587,10 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| 		}) | ||||
| 	}, context.RepoAssignment(), context.UnitTypes(), context.LoadRepoUnits(), context.CheckUnit(models.UnitTypeReleases)) | ||||
| 
 | ||||
| 	m.Group("/:username/:reponame", func() { | ||||
| 		m.Post("/topics", repo.TopicPost) | ||||
| 	}, context.RepoAssignment(), reqRepoAdmin) | ||||
| 
 | ||||
| 	m.Group("/:username/:reponame", func() { | ||||
| 		m.Group("", func() { | ||||
| 			m.Get("/^:type(issues|pulls)$", repo.RetrieveLabels, repo.Issues) | ||||
|  |  | |||
|  | @ -17,6 +17,9 @@ | |||
| 				</div> | ||||
| 			</div> | ||||
| 			{{if .DescriptionHTML}}<p class="has-emoji">{{.DescriptionHTML}}</p>{{end}} | ||||
| 			<div> | ||||
| 			{{range .Topics}}<div class="ui green basic label topic">{{.}}</div>{{end}} | ||||
| 			</div> | ||||
| 			<p class="time">{{$.i18n.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.i18n.Lang}}</p> | ||||
| 		</div> | ||||
| 	{{else}} | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 		{{template "base/alert" .}} | ||||
| 		<div class="ui repo-description"> | ||||
| 			<div id="repo-desc"> | ||||
| 				{{if .Repository.DescriptionHTML}}<span class="description has-emoji">{{.Repository.DescriptionHTML}}</span>{{else if .IsRepositoryAdmin}}<span class="no-description text-italic">{{.i18n.Tr "repo.no_desc"}}</span>{{end}} | ||||
| 				{{if .Repository.DescriptionHTML}}<span class="description has-emoji">{{.Repository.DescriptionHTML}}</span>{{else}}<span class="no-description text-italic">{{.i18n.Tr "repo.no_desc"}}</span>{{end}} | ||||
| 				<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a> | ||||
| 			</div> | ||||
| 			{{if .RepoSearchEnabled}} | ||||
|  | @ -23,6 +23,27 @@ | |||
| 				</div> | ||||
| 			{{end}} | ||||
| 		</div> | ||||
| 		<div class="ui repo-topic" id="repo-topic"> | ||||
| 		{{range .Topics}}<div class="ui green basic label topic" style="cursor:pointer;">{{.Name}}</div>{{end}} | ||||
| 		{{if .IsRepositoryAdmin}}<a id="manage_topic" style="cursor:pointer;margin-left:10px;">{{.i18n.Tr "repo.topic.manage_topics"}}</a>{{end}} | ||||
| 		</div> | ||||
| 		{{if .IsRepositoryAdmin}} | ||||
| 		<div class="ui repo-topic-edit grid" id="topic_edit" > | ||||
| 			<div class="fourteen wide column"> | ||||
| 				<div class="ui fluid multiple search selection dropdown"> | ||||
| 					<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if lt (Add $i 1) (len $.Topics)}},{{end}}{{end}}"> | ||||
| 					{{range .Topics}} | ||||
| 					<a class="ui green basic label topic transition visible" data-value="{{.Name}}" style="display: inline-block !important;">{{.Name}}<i class="delete icon"></i></a> | ||||
| 					{{end}} | ||||
| 					<div class="text"></div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="one wide column"> | ||||
| 				<a class="ui compact button primary" href="javascript:;" id="save_topic" | ||||
| 				data-link="{{.RepoLink}}/topics">{{.i18n.Tr "repo.topic.done"}}</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		{{end}} | ||||
| 		{{template "repo/sub_menu" .}} | ||||
| 		<div class="ui stackable secondary menu mobile--margin-between-items mobile--no-negative-margins"> | ||||
| 			{{if and .PullRequestCtx.Allowed .IsViewBranch}} | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue