mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Refactor repo-projects.ts (#32892)
- Remove jQuery - Add types to all functions - Tested all modified functionality --------- Co-authored-by: Giteabot <teabot@gitea.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -1020,14 +1020,15 @@ func registerRoutes(m *web.Router) { | |||||||
| 				m.Get("/new", org.RenderNewProject) | 				m.Get("/new", org.RenderNewProject) | ||||||
| 				m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) | 				m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) | ||||||
| 				m.Group("/{id}", func() { | 				m.Group("/{id}", func() { | ||||||
| 					m.Post("", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost) |  | ||||||
| 					m.Post("/move", project.MoveColumns) |  | ||||||
| 					m.Post("/delete", org.DeleteProject) | 					m.Post("/delete", org.DeleteProject) | ||||||
|  |  | ||||||
| 					m.Get("/edit", org.RenderEditProject) | 					m.Get("/edit", org.RenderEditProject) | ||||||
| 					m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost) | 					m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost) | ||||||
| 					m.Post("/{action:open|close}", org.ChangeProjectStatus) | 					m.Post("/{action:open|close}", org.ChangeProjectStatus) | ||||||
|  |  | ||||||
|  | 					// TODO: improper name. Others are "delete project", "edit project", but this one is "move columns" | ||||||
|  | 					m.Post("/move", project.MoveColumns) | ||||||
|  | 					m.Post("/columns/new", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost) | ||||||
| 					m.Group("/{columnID}", func() { | 					m.Group("/{columnID}", func() { | ||||||
| 						m.Put("", web.Bind(forms.EditProjectColumnForm{}), org.EditProjectColumn) | 						m.Put("", web.Bind(forms.EditProjectColumnForm{}), org.EditProjectColumn) | ||||||
| 						m.Delete("", org.DeleteProjectColumn) | 						m.Delete("", org.DeleteProjectColumn) | ||||||
| @@ -1387,14 +1388,15 @@ func registerRoutes(m *web.Router) { | |||||||
| 			m.Get("/new", repo.RenderNewProject) | 			m.Get("/new", repo.RenderNewProject) | ||||||
| 			m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) | 			m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) | ||||||
| 			m.Group("/{id}", func() { | 			m.Group("/{id}", func() { | ||||||
| 				m.Post("", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost) |  | ||||||
| 				m.Post("/move", project.MoveColumns) |  | ||||||
| 				m.Post("/delete", repo.DeleteProject) | 				m.Post("/delete", repo.DeleteProject) | ||||||
|  |  | ||||||
| 				m.Get("/edit", repo.RenderEditProject) | 				m.Get("/edit", repo.RenderEditProject) | ||||||
| 				m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost) | 				m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost) | ||||||
| 				m.Post("/{action:open|close}", repo.ChangeProjectStatus) | 				m.Post("/{action:open|close}", repo.ChangeProjectStatus) | ||||||
|  |  | ||||||
|  | 				// TODO: improper name. Others are "delete project", "edit project", but this one is "move columns" | ||||||
|  | 				m.Post("/move", project.MoveColumns) | ||||||
|  | 				m.Post("/columns/new", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost) | ||||||
| 				m.Group("/{columnID}", func() { | 				m.Group("/{columnID}", func() { | ||||||
| 					m.Put("", web.Bind(forms.EditProjectColumnForm{}), repo.EditProjectColumn) | 					m.Put("", web.Bind(forms.EditProjectColumnForm{}), repo.EditProjectColumn) | ||||||
| 					m.Delete("", repo.DeleteProjectColumn) | 					m.Delete("", repo.DeleteProjectColumn) | ||||||
|   | |||||||
| @@ -37,41 +37,25 @@ | |||||||
| 						{{ctx.Locale.Tr "repo.projects.close"}} | 						{{ctx.Locale.Tr "repo.projects.close"}} | ||||||
| 					</button> | 					</button> | ||||||
| 				{{end}} | 				{{end}} | ||||||
| 				<button class="item btn delete-button" data-url="{{.Link}}/delete" data-id="{{.Project.ID}}"> | 				<button class="item btn link-action" data-url="{{.Link}}/delete?id={{.Project.ID}}" | ||||||
|  | 								data-modal-confirm-header="{{ctx.Locale.Tr "repo.projects.deletion"}}" | ||||||
|  | 								data-modal-confirm-content="{{ctx.Locale.Tr "repo.projects.deletion_desc"}}" | ||||||
|  | 				> | ||||||
| 					{{svg "octicon-trash"}} | 					{{svg "octicon-trash"}} | ||||||
| 					{{ctx.Locale.Tr "repo.issues.label_delete"}} | 					{{ctx.Locale.Tr "repo.issues.label_delete"}} | ||||||
| 				</button> | 				</button> | ||||||
| 				<button class="item btn show-modal" data-modal="#new-project-column-item"> | 				<button class="item btn show-modal show-project-column-modal-edit" data-modal="#project-column-modal-edit" | ||||||
|  | 								data-modal-header="{{ctx.Locale.Tr "repo.projects.column.new"}}" | ||||||
|  | 								data-modal-project-column-title-label="{{ctx.Locale.Tr "repo.projects.column.new_title"}}" | ||||||
|  | 								data-modal-project-column-button-save="{{ctx.Locale.Tr "repo.projects.column.new_submit"}}" | ||||||
|  | 								data-modal-project-column-id="" | ||||||
|  | 								data-modal-project-column-title-input="" | ||||||
|  | 								data-modal-project-column-color-input="" | ||||||
|  | 				> | ||||||
| 					{{svg "octicon-plus"}} | 					{{svg "octicon-plus"}} | ||||||
| 					{{ctx.Locale.Tr "new_project_column"}} | 					{{ctx.Locale.Tr "new_project_column"}} | ||||||
| 				</button> | 				</button> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="ui small modal new-project-column-modal" id="new-project-column-item"> |  | ||||||
| 				<div class="header"> |  | ||||||
| 					{{ctx.Locale.Tr "repo.projects.column.new"}} |  | ||||||
| 				</div> |  | ||||||
| 				<div class="content"> |  | ||||||
| 					<form class="ui form"> |  | ||||||
| 						<div class="required field"> |  | ||||||
| 							<label for="new_project_column">{{ctx.Locale.Tr "repo.projects.column.new_title"}}</label> |  | ||||||
| 							<input class="new-project-column" id="new_project_column" name="title" required> |  | ||||||
| 						</div> |  | ||||||
|  |  | ||||||
| 						<div class="field color-field"> |  | ||||||
| 							<label for="new_project_column_color_picker">{{ctx.Locale.Tr "repo.projects.column.color"}}</label> |  | ||||||
| 							<div class="js-color-picker-input column"> |  | ||||||
| 								<input maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color"> |  | ||||||
| 								{{template "repo/issue/label_precolors"}} |  | ||||||
| 							</div> |  | ||||||
| 						</div> |  | ||||||
|  |  | ||||||
| 						<div class="text right actions"> |  | ||||||
| 							<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> |  | ||||||
| 							<button data-url="{{$.Link}}" class="ui primary button" id="new_project_column_submit">{{ctx.Locale.Tr "repo.projects.column.new_submit"}}</button> |  | ||||||
| 						</div> |  | ||||||
| 					</form> |  | ||||||
| 				</div> |  | ||||||
| 			</div> |  | ||||||
| 		{{end}} | 		{{end}} | ||||||
| 	</div> | 	</div> | ||||||
|  |  | ||||||
| @@ -80,88 +64,43 @@ | |||||||
| 	<div class="divider"></div> | 	<div class="divider"></div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div id="project-board"> | <div id="project-board" data-project-borad-writable="{{$canWriteProject}}"> | ||||||
| 	<div class="board {{if .CanWriteProjects}}sortable{{end}}"{{if .CanWriteProjects}} data-url="{{$.Link}}/move"{{end}}> | 	<div class="board {{if $canWriteProject}}sortable{{end}}" {{if $canWriteProject}}data-url="{{$.Link}}/move"{{end}}> | ||||||
| 		{{range .Columns}} | 		{{range .Columns}} | ||||||
| 			<div class="project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> | 			<div class="project-column" {{if .Color}}style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}"> | ||||||
| 				<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}"> | 				<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}"> | ||||||
| 					<div class="ui circular label project-column-issue-count"> | 					<div class="ui circular label project-column-issue-count"> | ||||||
| 						{{.NumIssues ctx}} | 						{{.NumIssues ctx}} | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class="project-column-title-label gt-ellipsis">{{.Title}}</div> | 					<div class="project-column-title-text gt-ellipsis">{{.Title}}</div> | ||||||
| 					{{if $canWriteProject}} | 					{{if $canWriteProject}} | ||||||
| 						<div class="ui dropdown tw-p-1"> | 						<div class="ui dropdown tw-p-1"> | ||||||
| 							{{svg "octicon-kebab-horizontal"}} | 							{{svg "octicon-kebab-horizontal"}} | ||||||
| 							<div class="menu"> | 							<div class="menu"> | ||||||
| 								<a class="item show-modal button" data-modal="#edit-project-column-modal-{{.ID}}"> | 								<a class="item button show-modal show-project-column-modal-edit" data-modal="#project-column-modal-edit" | ||||||
| 									{{svg "octicon-pencil"}} | 									data-modal-header="{{ctx.Locale.Tr "repo.projects.column.edit"}}" | ||||||
| 									{{ctx.Locale.Tr "repo.projects.column.edit"}} | 									data-modal-project-column-title-label="{{ctx.Locale.Tr "repo.projects.column.edit_title"}}" | ||||||
|  | 									data-modal-project-column-button-save="{{ctx.Locale.Tr "repo.projects.column.edit"}}" | ||||||
|  | 									data-modal-project-column-id="{{.ID}}" | ||||||
|  | 									data-modal-project-column-title-input="{{.Title}}" | ||||||
|  | 									data-modal-project-column-color-input="{{.Color}}" | ||||||
|  | 								> | ||||||
|  | 									{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.projects.column.edit"}} | ||||||
| 								</a> | 								</a> | ||||||
| 								{{if not .Default}} | 								{{if not .Default}} | ||||||
| 									<a class="item show-modal button default-project-column-show" | 									<a class="item button link-action" data-url="{{$.Link}}/{{.ID}}/default" | ||||||
| 										data-modal="#default-project-column-modal-{{.ID}}" | 										data-modal-confirm-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}" | ||||||
| 										data-modal-default-project-column-header="{{ctx.Locale.Tr "repo.projects.column.set_default"}}" | 										data-modal-confirm-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}" | ||||||
| 										data-modal-default-project-column-content="{{ctx.Locale.Tr "repo.projects.column.set_default_desc"}}" | 									> | ||||||
| 										data-url="{{$.Link}}/{{.ID}}/default"> | 										{{svg "octicon-pin"}} {{ctx.Locale.Tr "repo.projects.column.set_default"}} | ||||||
| 										{{svg "octicon-pin"}} |  | ||||||
| 										{{ctx.Locale.Tr "repo.projects.column.set_default"}} |  | ||||||
| 									</a> | 									</a> | ||||||
| 									<a class="item show-modal button show-delete-project-column-modal" | 									<a class="item button link-action" data-url="{{$.Link}}/{{.ID}}" data-link-action-method="DELETE" | ||||||
| 										data-modal="#delete-project-column-modal-{{.ID}}" | 										data-modal-confirm-header="{{ctx.Locale.Tr "repo.projects.column.delete"}}" | ||||||
| 										data-url="{{$.Link}}/{{.ID}}"> | 										data-modal-confirm-content="{{ctx.Locale.Tr "repo.projects.column.deletion_desc"}}" | ||||||
| 										{{svg "octicon-trash"}} | 									> | ||||||
| 										{{ctx.Locale.Tr "repo.projects.column.delete"}} | 										{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.projects.column.delete"}} | ||||||
| 									</a> | 									</a> | ||||||
| 								{{end}} | 								{{end}} | ||||||
|  |  | ||||||
| 								<div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}"> |  | ||||||
| 									<div class="header"> |  | ||||||
| 										{{ctx.Locale.Tr "repo.projects.column.edit"}} |  | ||||||
| 									</div> |  | ||||||
| 									<div class="content"> |  | ||||||
| 										<form class="ui form"> |  | ||||||
| 											<div class="required field"> |  | ||||||
| 												<label for="new_project_column_title">{{ctx.Locale.Tr "repo.projects.column.edit_title"}}</label> |  | ||||||
| 												<input class="project-column-title-input" id="new_project_column_title" name="title" value="{{.Title}}" required> |  | ||||||
| 											</div> |  | ||||||
|  |  | ||||||
| 											<div class="field color-field"> |  | ||||||
| 												<label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label> |  | ||||||
| 												<div class="js-color-picker-input column"> |  | ||||||
| 													<input maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}"> |  | ||||||
| 													{{template "repo/issue/label_precolors"}} |  | ||||||
| 												</div> |  | ||||||
| 											</div> |  | ||||||
|  |  | ||||||
| 											<div class="text right actions"> |  | ||||||
| 												<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> |  | ||||||
| 												<button data-url="{{$.Link}}/{{.ID}}" class="ui primary button edit-project-column-button">{{ctx.Locale.Tr "repo.projects.column.edit"}}</button> |  | ||||||
| 											</div> |  | ||||||
| 										</form> |  | ||||||
| 									</div> |  | ||||||
| 								</div> |  | ||||||
|  |  | ||||||
| 								<div class="ui g-modal-confirm modal default-project-column-modal" id="default-project-column-modal-{{.ID}}"> |  | ||||||
| 									<div class="header"> |  | ||||||
| 										<span id="default-project-column-header"></span> |  | ||||||
| 									</div> |  | ||||||
| 									<div class="content"> |  | ||||||
| 										<label id="default-project-column-content"></label> |  | ||||||
| 									</div> |  | ||||||
| 									{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} |  | ||||||
| 								</div> |  | ||||||
|  |  | ||||||
| 								<div class="ui g-modal-confirm modal" id="delete-project-column-modal-{{.ID}}"> |  | ||||||
| 									<div class="header"> |  | ||||||
| 										{{ctx.Locale.Tr "repo.projects.column.delete"}} |  | ||||||
| 									</div> |  | ||||||
| 									<div class="content"> |  | ||||||
| 										<label> |  | ||||||
| 											{{ctx.Locale.Tr "repo.projects.column.deletion_desc"}} |  | ||||||
| 										</label> |  | ||||||
| 									</div> |  | ||||||
| 									{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}} |  | ||||||
| 								</div> |  | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						</div> | ||||||
| 					{{end}} | 					{{end}} | ||||||
| @@ -179,15 +118,28 @@ | |||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| {{if .CanWriteProjects}} | {{if $canWriteProject}} | ||||||
| 	<div class="ui g-modal-confirm delete modal"> | <div class="ui small modal" id="project-column-modal-edit"> | ||||||
| 		<div class="header"> | 	<div class="header">edit</div> | ||||||
| 			{{svg "octicon-trash"}} | 	<div class="content"> | ||||||
| 			{{ctx.Locale.Tr "repo.projects.deletion"}} | 		<form class="ui form ignore-dirty" method="post" data-action-base-link="{{$.Link}}"> | ||||||
| 		</div> | 			<input class="project-column-id" type="hidden" name="id"> | ||||||
| 		<div class="content"> | 			<div class="required field"> | ||||||
| 			<p>{{ctx.Locale.Tr "repo.projects.deletion_desc"}}</p> | 				<label class="project-column-title-label" for="project-column-title-input">title</label> | ||||||
| 		</div> | 				<input id="project-column-title-input" name="title" value="{{.Title}}" required> | ||||||
| 		{{template "base/modal_actions_confirm" .}} | 			</div> | ||||||
|  | 			<div class="field"> | ||||||
|  | 				<label class="project-column-color-label" for="project-column-color-input">color</label> | ||||||
|  | 				<div class="js-color-picker-input column"> | ||||||
|  | 					<input maxlength="7" placeholder="#c320f6" id="project-column-color-input" name="color" value="{{.Color}}"> | ||||||
|  | 					{{template "repo/issue/label_precolors"}} | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="actions tw-text-right"> | ||||||
|  | 				<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> | ||||||
|  | 				<button type="submit" class="ui primary button project-column-button-save">save</button> | ||||||
|  | 			</div> | ||||||
|  | 		</form> | ||||||
| 	</div> | 	</div> | ||||||
|  | </div> | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ | |||||||
| 					<input class="label-desc-input" name="description" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200"> | 					<input class="label-desc-input" name="description" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200"> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="field color-field"> | 			<div class="field"> | ||||||
| 				<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label> | 				<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label> | ||||||
| 				<div class="column js-color-picker-input"> | 				<div class="column js-color-picker-input"> | ||||||
| 					<input name="color" value="#70c24a"placeholder="#c320f6" required maxlength="7"> | 					<input name="color" value="#70c24a"placeholder="#c320f6" required maxlength="7"> | ||||||
|   | |||||||
| @@ -51,10 +51,6 @@ | |||||||
|   color: inherit; |   color: inherit; | ||||||
| } | } | ||||||
|  |  | ||||||
| .project-column-title-label { |  | ||||||
|   flex: 1; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .project-column > .cards { | .project-column > .cards { | ||||||
|   flex: 1; |   flex: 1; | ||||||
|   display: flex; |   display: flex; | ||||||
|   | |||||||
| @@ -100,7 +100,7 @@ async function linkAction(el: HTMLElement, e: Event) { | |||||||
|   const url = el.getAttribute('data-url'); |   const url = el.getAttribute('data-url'); | ||||||
|   const doRequest = async () => { |   const doRequest = async () => { | ||||||
|     if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but A doesn't have disabled attribute |     if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but A doesn't have disabled attribute | ||||||
|     await fetchActionDoRequest(el, url, {method: 'POST'}); |     await fetchActionDoRequest(el, url, {method: el.getAttribute('data-link-action-method') || 'POST'}); | ||||||
|     if ('disabled' in el) el.disabled = false; |     if ('disabled' in el) el.disabled = false; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,31 +1,17 @@ | |||||||
| import $ from 'jquery'; |  | ||||||
| import {contrastColor} from '../utils/color.ts'; | import {contrastColor} from '../utils/color.ts'; | ||||||
| import {createSortable} from '../modules/sortable.ts'; | import {createSortable} from '../modules/sortable.ts'; | ||||||
| import {POST, DELETE, PUT} from '../modules/fetch.ts'; | import {POST, request} from '../modules/fetch.ts'; | ||||||
|  | import {fomanticQuery} from '../modules/fomantic/base.ts'; | ||||||
|  | import {queryElemChildren, queryElems} from '../utils/dom.ts'; | ||||||
|  | import type {SortableEvent} from 'sortablejs'; | ||||||
|  |  | ||||||
| function updateIssueCount(cards) { | function updateIssueCount(card: HTMLElement): void { | ||||||
|   const parent = cards.parentElement; |   const parent = card.parentElement; | ||||||
|   const cnt = parent.querySelectorAll('.issue-card').length; |   const count = parent.querySelectorAll('.issue-card').length; | ||||||
|   parent.querySelectorAll('.project-column-issue-count')[0].textContent = cnt; |   parent.querySelector('.project-column-issue-count').textContent = String(count); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function createNewColumn(url, columnTitle, projectColorInput) { | async function moveIssue({item, from, to, oldIndex}: SortableEvent): Promise<void> { | ||||||
|   try { |  | ||||||
|     await POST(url, { |  | ||||||
|       data: { |  | ||||||
|         title: columnTitle.val(), |  | ||||||
|         color: projectColorInput.val(), |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   } catch (error) { |  | ||||||
|     console.error(error); |  | ||||||
|   } finally { |  | ||||||
|     columnTitle.closest('form').removeClass('dirty'); |  | ||||||
|     window.location.reload(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function moveIssue({item, from, to, oldIndex}: {item: HTMLElement, from: HTMLElement, to: HTMLElement, oldIndex: number}) { |  | ||||||
|   const columnCards = to.querySelectorAll('.issue-card'); |   const columnCards = to.querySelectorAll('.issue-card'); | ||||||
|   updateIssueCount(from); |   updateIssueCount(from); | ||||||
|   updateIssueCount(to); |   updateIssueCount(to); | ||||||
| @@ -47,13 +33,10 @@ async function moveIssue({item, from, to, oldIndex}: {item: HTMLElement, from: H | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| async function initRepoProjectSortable() { | async function initRepoProjectSortable(): Promise<void> { | ||||||
|   const els = document.querySelectorAll('#project-board > .board.sortable'); |  | ||||||
|   if (!els.length) return; |  | ||||||
|  |  | ||||||
|   // the HTML layout is: #project-board > .board > .project-column .cards > .issue-card |   // the HTML layout is: #project-board > .board > .project-column .cards > .issue-card | ||||||
|   const mainBoard = els[0]; |   const mainBoard = document.querySelector('#project-board > .board.sortable'); | ||||||
|   let boardColumns = mainBoard.querySelectorAll('.project-column'); |   let boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column'); | ||||||
|   createSortable(mainBoard, { |   createSortable(mainBoard, { | ||||||
|     group: 'project-column', |     group: 'project-column', | ||||||
|     draggable: '.project-column', |     draggable: '.project-column', | ||||||
| @@ -61,7 +44,7 @@ async function initRepoProjectSortable() { | |||||||
|     delayOnTouchOnly: true, |     delayOnTouchOnly: true, | ||||||
|     delay: 500, |     delay: 500, | ||||||
|     onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises |     onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises | ||||||
|       boardColumns = mainBoard.querySelectorAll('.project-column'); |       boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column'); | ||||||
|  |  | ||||||
|       const columnSorting = { |       const columnSorting = { | ||||||
|         columns: Array.from(boardColumns, (column, i) => ({ |         columns: Array.from(boardColumns, (column, i) => ({ | ||||||
| @@ -81,7 +64,7 @@ async function initRepoProjectSortable() { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   for (const boardColumn of boardColumns) { |   for (const boardColumn of boardColumns) { | ||||||
|     const boardCardList = boardColumn.querySelectorAll('.cards')[0]; |     const boardCardList = boardColumn.querySelector('.cards'); | ||||||
|     createSortable(boardCardList, { |     createSortable(boardCardList, { | ||||||
|       group: 'shared', |       group: 'shared', | ||||||
|       onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises |       onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises | ||||||
| @@ -92,97 +75,74 @@ async function initRepoProjectSortable() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export function initRepoProject() { | function initRepoProjectColumnEdit(writableProjectBoard: Element): void { | ||||||
|   if (!document.querySelector('.repository.projects')) { |   const elModal = document.querySelector<HTMLElement>('.ui.modal#project-column-modal-edit'); | ||||||
|     return; |   const elForm = elModal.querySelector<HTMLFormElement>('form'); | ||||||
|   } |  | ||||||
|  |  | ||||||
|   initRepoProjectSortable(); // no await |   const elColumnId = elForm.querySelector<HTMLInputElement>('input[name="id"]'); | ||||||
|  |   const elColumnTitle = elForm.querySelector<HTMLInputElement>('input[name="title"]'); | ||||||
|  |   const elColumnColor = elForm.querySelector<HTMLInputElement>('input[name="color"]'); | ||||||
|  |  | ||||||
|   for (const modal of document.querySelectorAll('.edit-project-column-modal')) { |   const attrDataColumnId = 'data-modal-project-column-id'; | ||||||
|     const projectHeader = modal.closest<HTMLElement>('.project-column-header'); |   const attrDataColumnTitle = 'data-modal-project-column-title-input'; | ||||||
|     const projectTitleLabel = projectHeader?.querySelector<HTMLElement>('.project-column-title-label'); |   const attrDataColumnColor = 'data-modal-project-column-color-input'; | ||||||
|     const projectTitleInput = modal.querySelector<HTMLInputElement>('.project-column-title-input'); |  | ||||||
|     const projectColorInput = modal.querySelector<HTMLInputElement>('#new_project_column_color'); |  | ||||||
|     const boardColumn = modal.closest<HTMLElement>('.project-column'); |  | ||||||
|     modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) { |  | ||||||
|       e.preventDefault(); |  | ||||||
|       try { |  | ||||||
|         await PUT(this.getAttribute('data-url'), { |  | ||||||
|           data: { |  | ||||||
|             title: projectTitleInput?.value, |  | ||||||
|             color: projectColorInput?.value, |  | ||||||
|           }, |  | ||||||
|         }); |  | ||||||
|       } catch (error) { |  | ||||||
|         console.error(error); |  | ||||||
|       } finally { |  | ||||||
|         projectTitleLabel.textContent = projectTitleInput?.value; |  | ||||||
|         projectTitleInput.closest('form')?.classList.remove('dirty'); |  | ||||||
|         const dividers = boardColumn.querySelectorAll<HTMLElement>(':scope > .divider'); |  | ||||||
|         if (projectColorInput.value) { |  | ||||||
|           const color = contrastColor(projectColorInput.value); |  | ||||||
|           boardColumn.style.setProperty('background', projectColorInput.value, 'important'); |  | ||||||
|           boardColumn.style.setProperty('color', color, 'important'); |  | ||||||
|           for (const divider of dividers) { |  | ||||||
|             divider.style.setProperty('color', color); |  | ||||||
|           } |  | ||||||
|         } else { |  | ||||||
|           boardColumn.style.removeProperty('background'); |  | ||||||
|           boardColumn.style.removeProperty('color'); |  | ||||||
|           for (const divider of dividers) { |  | ||||||
|             divider.style.removeProperty('color'); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         $('.ui.modal').modal('hide'); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   $('.default-project-column-modal').each(function () { |   // the "new" button is not in project board, so need to query from document | ||||||
|     const $boardColumn = $(this).closest('.project-column'); |   queryElems(document, '.show-project-column-modal-edit', (el) => { | ||||||
|     const $showButton = $($boardColumn).find('.default-project-column-show'); |     el.addEventListener('click', () => { | ||||||
|     const $commitButton = $(this).find('.actions > .ok.button'); |       elColumnId.value = el.getAttribute(attrDataColumnId); | ||||||
|  |       elColumnTitle.value = el.getAttribute(attrDataColumnTitle); | ||||||
|     $($commitButton).on('click', async (e) => { |       elColumnColor.value = el.getAttribute(attrDataColumnColor); | ||||||
|       e.preventDefault(); |       elColumnColor.dispatchEvent(new Event('input', {bubbles: true})); // trigger the color picker | ||||||
|  |  | ||||||
|       try { |  | ||||||
|         await POST($($showButton).data('url')); |  | ||||||
|       } catch (error) { |  | ||||||
|         console.error(error); |  | ||||||
|       } finally { |  | ||||||
|         window.location.reload(); |  | ||||||
|       } |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   $('.show-delete-project-column-modal').each(function () { |   elForm.addEventListener('submit', async (e) => { | ||||||
|     const $deleteColumnModal = $(`${this.getAttribute('data-modal')}`); |  | ||||||
|     const $deleteColumnButton = $deleteColumnModal.find('.actions > .ok.button'); |  | ||||||
|     const deleteUrl = this.getAttribute('data-url'); |  | ||||||
|  |  | ||||||
|     $deleteColumnButton.on('click', async (e) => { |  | ||||||
|       e.preventDefault(); |  | ||||||
|  |  | ||||||
|       try { |  | ||||||
|         await DELETE(deleteUrl); |  | ||||||
|       } catch (error) { |  | ||||||
|         console.error(error); |  | ||||||
|       } finally { |  | ||||||
|         window.location.reload(); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   $('#new_project_column_submit').on('click', (e) => { |  | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     const $columnTitle = $('#new_project_column'); |     const columnId = elColumnId.value; | ||||||
|     const $projectColorInput = $('#new_project_column_color_picker'); |     const actionBaseLink = elForm.getAttribute('data-action-base-link'); | ||||||
|     if (!$columnTitle.val()) { |  | ||||||
|       return; |     const formData = new FormData(elForm); | ||||||
|  |     const formLink = columnId ? `${actionBaseLink}/${columnId}` : `${actionBaseLink}/columns/new`; | ||||||
|  |     const formMethod = columnId ? 'PUT' : 'POST'; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       elForm.classList.add('is-loading'); | ||||||
|  |       await request(formLink, {method: formMethod, data: formData}); | ||||||
|  |       if (!columnId) { | ||||||
|  |         window.location.reload(); // newly added column, need to reload the page | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       fomanticQuery(elModal).modal('hide'); | ||||||
|  |  | ||||||
|  |       // update the newly saved column title and color in the project board (to avoid reload) | ||||||
|  |       const elEditButton = writableProjectBoard.querySelector<HTMLButtonElement>(`.show-project-column-modal-edit[${attrDataColumnId}="${columnId}"]`); | ||||||
|  |       elEditButton.setAttribute(attrDataColumnTitle, elColumnTitle.value); | ||||||
|  |       elEditButton.setAttribute(attrDataColumnColor, elColumnColor.value); | ||||||
|  |  | ||||||
|  |       const elBoardColumn = writableProjectBoard.querySelector<HTMLElement>(`.project-column[data-id="${columnId}"]`); | ||||||
|  |       const elBoardColumnTitle = elBoardColumn.querySelector<HTMLElement>(`.project-column-title-text`); | ||||||
|  |       elBoardColumnTitle.textContent = elColumnTitle.value; | ||||||
|  |       if (elColumnColor.value) { | ||||||
|  |         const textColor = contrastColor(elColumnColor.value); | ||||||
|  |         elBoardColumn.style.setProperty('background', elColumnColor.value, 'important'); | ||||||
|  |         elBoardColumn.style.setProperty('color', textColor, 'important'); | ||||||
|  |         queryElemChildren<HTMLElement>(elBoardColumn, '.divider', (divider) => divider.style.color = textColor); | ||||||
|  |       } else { | ||||||
|  |         elBoardColumn.style.removeProperty('background'); | ||||||
|  |         elBoardColumn.style.removeProperty('color'); | ||||||
|  |         queryElemChildren<HTMLElement>(elBoardColumn, '.divider', (divider) => divider.style.removeProperty('color')); | ||||||
|  |       } | ||||||
|  |     } finally { | ||||||
|  |       elForm.classList.remove('is-loading'); | ||||||
|     } |     } | ||||||
|     const url = e.target.getAttribute('data-url'); |  | ||||||
|     createNewColumn(url, $columnTitle, $projectColorInput); |  | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function initRepoProject(): void { | ||||||
|  |   const writableProjectBoard = document.querySelector('#project-board[data-project-borad-writable="true"]'); | ||||||
|  |   if (!writableProjectBoard) return; | ||||||
|  |  | ||||||
|  |   initRepoProjectSortable(); // no await | ||||||
|  |   initRepoProjectColumnEdit(writableProjectBoard); | ||||||
|  | } | ||||||
|   | |||||||
| @@ -2,7 +2,8 @@ import {beforeEach, describe, expect, test, vi} from 'vitest'; | |||||||
| import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts'; | import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts'; | ||||||
| import {POST} from '../modules/fetch.ts'; | import {POST} from '../modules/fetch.ts'; | ||||||
| import {createSortable} from '../modules/sortable.ts'; | import {createSortable} from '../modules/sortable.ts'; | ||||||
| import type {SortableEvent} from 'sortablejs'; | import type {SortableEvent, SortableOptions} from 'sortablejs'; | ||||||
|  | import type Sortable from 'sortablejs'; | ||||||
|  |  | ||||||
| vi.mock('../modules/fetch.ts', () => ({ | vi.mock('../modules/fetch.ts', () => ({ | ||||||
|   POST: vi.fn(), |   POST: vi.fn(), | ||||||
| @@ -55,9 +56,10 @@ describe('Repository Branch Settings', () => { | |||||||
|     vi.mocked(POST).mockResolvedValue({ok: true} as Response); |     vi.mocked(POST).mockResolvedValue({ok: true} as Response); | ||||||
|  |  | ||||||
|     // Mock createSortable to capture and execute the onEnd callback |     // Mock createSortable to capture and execute the onEnd callback | ||||||
|     vi.mocked(createSortable).mockImplementation(async (_el: Element, options) => { |     vi.mocked(createSortable).mockImplementation(async (_el: Element, options: SortableOptions) => { | ||||||
|       options.onEnd(new Event('SortableEvent') as SortableEvent); |       options.onEnd(new Event('SortableEvent') as SortableEvent); | ||||||
|       return {destroy: vi.fn()}; |       // @ts-expect-error: mock is incomplete | ||||||
|  |       return {destroy: vi.fn()} as Sortable; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     initRepoSettingsBranchesDrag(); |     initRepoSettingsBranchesDrag(); | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import type {SortableOptions, SortableEvent} from 'sortablejs'; | import type {SortableOptions, SortableEvent} from 'sortablejs'; | ||||||
|  | import type SortableType from 'sortablejs'; | ||||||
|  |  | ||||||
| export async function createSortable(el: Element, opts: {handle?: string} & SortableOptions = {}) { | export async function createSortable(el: Element, opts: {handle?: string} & SortableOptions = {}): Promise<SortableType> { | ||||||
|   // @ts-expect-error: wrong type derived by typescript |   // @ts-expect-error: wrong type derived by typescript | ||||||
|   const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs'); |   const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs'); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user