From aee9801d468997ab3cce32978416b697d9df77a7 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 31 Oct 2024 02:36:02 +0800 Subject: [PATCH 1/4] Fix toAbsoluteLocaleDate and add more tests (#32387) --- .../js/webcomponents/absolute-date.test.ts | 15 ++++++++++----- web_src/js/webcomponents/absolute-date.ts | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/web_src/js/webcomponents/absolute-date.test.ts b/web_src/js/webcomponents/absolute-date.test.ts index b7a2354e0dd..a3866829a79 100644 --- a/web_src/js/webcomponents/absolute-date.test.ts +++ b/web_src/js/webcomponents/absolute-date.test.ts @@ -13,9 +13,14 @@ test('toAbsoluteLocaleDate', () => { day: 'numeric', })).toEqual('15. März 2024'); - expect(toAbsoluteLocaleDate('12345-03-15 01:02:03', '', { - year: 'numeric', - month: 'short', - day: 'numeric', - })).toEqual('Mar 15, 12345'); + // these cases shouldn't happen + expect(toAbsoluteLocaleDate('2024-03-15 01:02:03', '', {})).toEqual('Invalid Date'); + expect(toAbsoluteLocaleDate('10000-01-01', '', {})).toEqual('Invalid Date'); + + // test different timezone + const oldTZ = process.env.TZ; + process.env.TZ = 'America/New_York'; + expect(new Date('2024-03-15').toLocaleString()).toEqual('3/14/2024, 8:00:00 PM'); + expect(toAbsoluteLocaleDate('2024-03-15')).toEqual('3/15/2024, 12:00:00 AM'); + process.env.TZ = oldTZ; }); diff --git a/web_src/js/webcomponents/absolute-date.ts b/web_src/js/webcomponents/absolute-date.ts index a215722511d..6a053c6a55c 100644 --- a/web_src/js/webcomponents/absolute-date.ts +++ b/web_src/js/webcomponents/absolute-date.ts @@ -1,5 +1,12 @@ -export function toAbsoluteLocaleDate(date: string, lang: string, opts: Intl.DateTimeFormatOptions) { - return new Date(date).toLocaleString(lang || [], opts); +export function toAbsoluteLocaleDate(date: string, lang?: string, opts?: Intl.DateTimeFormatOptions) { + // only use the date part, it is guaranteed to be in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ) or (YYYY-MM-DD) + // if there is an "Invalid Date" error, there must be something wrong in code and should be fixed. + // TODO: there is a root problem in backend code: the date "YYYY-MM-DD" is passed to backend without timezone (eg: deadline), + // then backend parses it in server's timezone and stores the parsed timestamp into database. + // If the user's timezone is different from the server's, the date might be displayed in the wrong day. + const dateSep = date.indexOf('T'); + date = dateSep === -1 ? date : date.substring(0, dateSep); + return new Date(`${date}T00:00:00`).toLocaleString(lang || [], opts); } window.customElements.define('absolute-date', class extends HTMLElement { @@ -15,14 +22,8 @@ window.customElements.define('absolute-date', class extends HTMLElement { const lang = this.closest('[lang]')?.getAttribute('lang') || this.ownerDocument.documentElement.getAttribute('lang') || ''; - // only use the date part, it is guaranteed to be in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ) - let date = this.getAttribute('date'); - let dateSep = date.indexOf('T'); - dateSep = dateSep === -1 ? date.indexOf(' ') : dateSep; - date = dateSep === -1 ? date : date.substring(0, dateSep); - if (!this.shadowRoot) this.attachShadow({mode: 'open'}); - this.shadowRoot.textContent = toAbsoluteLocaleDate(date, lang, opt); + this.shadowRoot.textContent = toAbsoluteLocaleDate(this.getAttribute('date'), lang, opt); }; attributeChangedCallback(_name, oldValue, newValue) { From dd1f67491f5e2f798a537a61c082b1bf12e47635 Mon Sep 17 00:00:00 2001 From: yp05327 <576951401@qq.com> Date: Thu, 31 Oct 2024 04:05:40 +0900 Subject: [PATCH 2/4] Fix the missing menu in organization project view page (#32313) #29248 didn't modify the view page. The class name is not good enough, so this is a quick fix. Before: org: ![image](https://github.com/user-attachments/assets/3e26502d-66b4-4043-ab03-003ba7391487) user: ![image](https://github.com/user-attachments/assets/9b22b90c-d63c-4228-acad-4d9fb20590ac) After: org: ![image](https://github.com/user-attachments/assets/21bf98a7-8a5b-4dc6-950a-88f529e36450) user: (no change) ![image](https://github.com/user-attachments/assets/fea0dcae-3625-44e8-bb9e-4c3733da8764) Co-authored-by: Giteabot --- templates/org/projects/view.tmpl | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/templates/org/projects/view.tmpl b/templates/org/projects/view.tmpl index e1ab81c4cde..bd74114fe2e 100644 --- a/templates/org/projects/view.tmpl +++ b/templates/org/projects/view.tmpl @@ -1,9 +1,13 @@ {{template "base/head" .}} -
- {{template "shared/user/org_profile_avatar" .}} -
- {{template "user/overview/header" .}} -
+
+ {{if .ContextUser.IsOrganization}} + {{template "org/header" .}} + {{else}} + {{template "shared/user/org_profile_avatar" .}} +
+ {{template "user/overview/header" .}} +
+ {{end}}
{{template "projects/view" .}}
From f4d3aaeeb9e1b11c5495e4608a3f52f316c35758 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Wed, 30 Oct 2024 21:36:24 +0200 Subject: [PATCH 3/4] refactor: remove redundant err declarations (#32381) --- build/generate-emoji.go | 4 ---- models/git/lfs.go | 2 -- models/issues/label_test.go | 3 +-- modules/charset/charset_test.go | 4 +--- modules/markup/markdown/goldmark.go | 3 +-- routers/api/v1/repo/repo.go | 1 - routers/api/v1/user/repo.go | 1 - routers/web/repo/actions/view.go | 1 - routers/web/repo/activity.go | 1 - routers/web/repo/view.go | 1 - services/issue/template.go | 2 -- 11 files changed, 3 insertions(+), 20 deletions(-) diff --git a/build/generate-emoji.go b/build/generate-emoji.go index 17a9670f06a..446ab5f4405 100644 --- a/build/generate-emoji.go +++ b/build/generate-emoji.go @@ -53,8 +53,6 @@ func (e Emoji) MarshalJSON() ([]byte, error) { } func main() { - var err error - flag.Parse() // generate data @@ -83,8 +81,6 @@ var replacer = strings.NewReplacer( var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`) func generate() ([]byte, error) { - var err error - // load gemoji data res, err := http.Get(gemojiURL) if err != nil { diff --git a/models/git/lfs.go b/models/git/lfs.go index 837dc9fd312..bb6361050aa 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -136,8 +136,6 @@ var ErrLFSObjectNotExist = db.ErrNotExist{Resource: "LFS Meta object"} // NewLFSMetaObject stores a given populated LFSMetaObject structure in the database // if it is not already present. func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMetaObject, error) { - var err error - ctx, committer, err := db.TxContext(ctx) if err != nil { return nil, err diff --git a/models/issues/label_test.go b/models/issues/label_test.go index 396de809e1d..a0cc8e6d756 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -229,8 +229,7 @@ func TestGetLabelsByOrgID(t *testing.T) { testSuccess(3, "reversealphabetically", []int64{4, 3}) testSuccess(3, "default", []int64{3, 4}) - var err error - _, err = issues_model.GetLabelsByOrgID(db.DefaultContext, 0, "leastissues", db.ListOptions{}) + _, err := issues_model.GetLabelsByOrgID(db.DefaultContext, 0, "leastissues", db.ListOptions{}) assert.True(t, issues_model.IsErrOrgLabelNotExist(err)) _, err = issues_model.GetLabelsByOrgID(db.DefaultContext, -1, "leastissues", db.ListOptions{}) diff --git a/modules/charset/charset_test.go b/modules/charset/charset_test.go index 829844a9766..19b13033657 100644 --- a/modules/charset/charset_test.go +++ b/modules/charset/charset_test.go @@ -40,14 +40,12 @@ func TestMaybeRemoveBOM(t *testing.T) { func TestToUTF8(t *testing.T) { resetDefaultCharsetsOrder() - var res string - var err error // Note: golang compiler seems so behave differently depending on the current // locale, so some conversions might behave differently. For that reason, we don't // depend on particular conversions but in expected behaviors. - res, err = ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{}) + res, err := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{}) assert.NoError(t, err) assert.Equal(t, "ABC", res) diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index a89670eeefd..515a79578de 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -213,8 +213,7 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node return ast.WalkContinue, nil } - var err error - _, err = w.WriteString(fmt.Sprintf(``, name)) + _, err := w.WriteString(fmt.Sprintf(``, name)) if err != nil { return ast.WalkStop, err } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 4638e2ba5c3..698ba3cc949 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -202,7 +202,6 @@ func Search(ctx *context.APIContext) { } } - var err error repos, count, err := repo_model.SearchRepository(ctx, opts) if err != nil { ctx.JSON(http.StatusInternalServerError, api.SearchError{ diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index d0264d6b5a2..61113414231 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -111,7 +111,6 @@ func ListMyRepos(ctx *context.APIContext) { IncludeDescription: true, } - var err error repos, count, err := repo_model.SearchRepository(ctx, opts) if err != nil { ctx.Error(http.StatusInternalServerError, "SearchRepository", err) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 11199d69eb3..20e29425a39 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -276,7 +276,6 @@ func ViewPost(ctx *context_module.Context) { if validCursor { length := step.LogLength - cursor.Cursor offset := task.LogIndexes[index] - var err error logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length) if err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go index c8fa60f77a7..4b14c28b3e6 100644 --- a/routers/web/repo/activity.go +++ b/routers/web/repo/activity.go @@ -94,7 +94,6 @@ func ActivityAuthors(ctx *context.Context) { timeFrom = timeUntil.Add(-time.Hour * 168) } - var err error authors, err := activities_model.GetActivityStatsTopAuthors(ctx, ctx.Repo.Repository, timeFrom, 10) if err != nil { ctx.ServerError("GetActivityStatsTopAuthors", err) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 97691176094..7d9281b3973 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -143,7 +143,6 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try // this should be impossible; if subTreeEntry exists so should this. continue } - var err error childEntries, err := subTree.ListEntries() if err != nil { return "", nil, err diff --git a/services/issue/template.go b/services/issue/template.go index dd9d015f0fa..4b0f1aa9870 100644 --- a/services/issue/template.go +++ b/services/issue/template.go @@ -52,8 +52,6 @@ func GetTemplateConfig(gitRepo *git.Repository, path string, commit *git.Commit) return GetDefaultTemplateConfig(), nil } - var err error - treeEntry, err := commit.GetTreeEntryByPath(path) if err != nil { return GetDefaultTemplateConfig(), err From a4a121c684fdbbad19971ff658166fd47932b192 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 31 Oct 2024 04:06:36 +0800 Subject: [PATCH 4/4] Fix suggestions for issues (#32380) --- routers/web/repo/issue_suggestions.go | 27 +++++------------ web_src/js/components/ContextPopup.vue | 7 +++-- web_src/js/features/comp/TextExpander.ts | 36 ++++++++-------------- web_src/js/features/contextpopup.ts | 10 +++---- web_src/js/svg.ts | 3 +- web_src/js/types.ts | 11 +++---- web_src/js/utils.test.ts | 38 ++++++++++++++---------- web_src/js/utils.ts | 14 ++++++--- web_src/js/utils/dom.test.ts | 6 ++-- web_src/js/utils/dom.ts | 12 ++++---- web_src/js/utils/match.ts | 6 ++-- 11 files changed, 82 insertions(+), 88 deletions(-) diff --git a/routers/web/repo/issue_suggestions.go b/routers/web/repo/issue_suggestions.go index 361da0ee609..46e9f339a5a 100644 --- a/routers/web/repo/issue_suggestions.go +++ b/routers/web/repo/issue_suggestions.go @@ -11,19 +11,10 @@ import ( "code.gitea.io/gitea/models/unit" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/context" ) -type issueSuggestion struct { - ID int64 `json:"id"` - Title string `json:"title"` - State string `json:"state"` - PullRequest *struct { - Merged bool `json:"merged"` - Draft bool `json:"draft"` - } `json:"pull_request,omitempty"` -} - // IssueSuggestions returns a list of issue suggestions func IssueSuggestions(ctx *context.Context) { keyword := ctx.Req.FormValue("q") @@ -61,13 +52,14 @@ func IssueSuggestions(ctx *context.Context) { return } - suggestions := make([]*issueSuggestion, 0, len(issues)) + suggestions := make([]*structs.Issue, 0, len(issues)) for _, issue := range issues { - suggestion := &issueSuggestion{ + suggestion := &structs.Issue{ ID: issue.ID, + Index: issue.Index, Title: issue.Title, - State: string(issue.State()), + State: issue.State(), } if issue.IsPull { @@ -76,12 +68,9 @@ func IssueSuggestions(ctx *context.Context) { return } if issue.PullRequest != nil { - suggestion.PullRequest = &struct { - Merged bool `json:"merged"` - Draft bool `json:"draft"` - }{ - Merged: issue.PullRequest.HasMerged, - Draft: issue.PullRequest.IsWorkInProgress(ctx), + suggestion.PullRequest = &structs.PullRequestMeta{ + HasMerged: issue.PullRequest.HasMerged, + IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx), } } } diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 8c56af858e3..b0e8447302f 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -3,6 +3,7 @@ import {SvgIcon} from '../svg.ts'; import {GET} from '../modules/fetch.ts'; import {getIssueColor, getIssueIcon} from '../features/issue.ts'; import {computed, onMounted, ref} from 'vue'; +import type {IssuePathInfo} from '../types.ts'; const {appSubUrl, i18n} = window.config; @@ -25,19 +26,19 @@ const root = ref(null); onMounted(() => { root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => { - const data = e.detail; + const data: IssuePathInfo = e.detail; if (!loading.value && issue.value === null) { load(data); } }); }); -async function load(data) { +async function load(issuePathInfo: IssuePathInfo) { loading.value = true; i18nErrorMessage.value = null; try { - const response = await GET(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`); // backend: GetIssueInfo + const response = await GET(`${appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`); // backend: GetIssueInfo const respJson = await response.json(); if (!response.ok) { i18nErrorMessage.value = respJson.message ?? i18n.network_error; diff --git a/web_src/js/features/comp/TextExpander.ts b/web_src/js/features/comp/TextExpander.ts index 6ac4c4bf32c..e0c4abed75b 100644 --- a/web_src/js/features/comp/TextExpander.ts +++ b/web_src/js/features/comp/TextExpander.ts @@ -1,39 +1,29 @@ import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts'; import {emojiString} from '../emoji.ts'; import {svg} from '../../svg.ts'; -import {parseIssueHref} from '../../utils.ts'; +import {parseIssueHref, parseIssueNewHref} from '../../utils.ts'; import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts'; import {getIssueColor, getIssueIcon} from '../issue.ts'; import {debounce} from 'perfect-debounce'; const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => { - const {owner, repo, index} = parseIssueHref(window.location.href); - const matches = await matchIssue(owner, repo, index, text); + let issuePathInfo = parseIssueHref(window.location.href); + if (!issuePathInfo.ownerName) issuePathInfo = parseIssueNewHref(window.location.href); + if (!issuePathInfo.ownerName) return resolve({matched: false}); + + const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text); if (!matches.length) return resolve({matched: false}); - const ul = document.createElement('ul'); - ul.classList.add('suggestions'); + const ul = createElementFromAttrs('ul', {class: 'suggestions'}); for (const issue of matches) { - const li = createElementFromAttrs('li', { - role: 'option', - 'data-value': `${key}${issue.id}`, - class: 'tw-flex tw-gap-2', - }); - - const icon = svg(getIssueIcon(issue), 16, ['text', getIssueColor(issue)].join(' ')); - li.append(createElementFromHTML(icon)); - - const id = document.createElement('span'); - id.textContent = issue.id.toString(); - li.append(id); - - const nameSpan = document.createElement('span'); - nameSpan.textContent = issue.title; - li.append(nameSpan); - + const li = createElementFromAttrs( + 'li', {role: 'option', class: 'tw-flex tw-gap-2', 'data-value': `${key}${issue.number}`}, + createElementFromHTML(svg(getIssueIcon(issue), 16, ['text', getIssueColor(issue)])), + createElementFromAttrs('span', null, `#${issue.number}`), + createElementFromAttrs('span', null, issue.title), + ); ul.append(li); } - resolve({matched: true, fragment: ul}); }), 100); diff --git a/web_src/js/features/contextpopup.ts b/web_src/js/features/contextpopup.ts index 5af7d176b6f..33eead8431c 100644 --- a/web_src/js/features/contextpopup.ts +++ b/web_src/js/features/contextpopup.ts @@ -10,12 +10,10 @@ export function initContextPopups() { export function attachRefIssueContextPopup(refIssues) { for (const refIssue of refIssues) { - if (refIssue.classList.contains('ref-external-issue')) { - return; - } + if (refIssue.classList.contains('ref-external-issue')) continue; - const {owner, repo, index} = parseIssueHref(refIssue.getAttribute('href')); - if (!owner) return; + const issuePathInfo = parseIssueHref(refIssue.getAttribute('href')); + if (!issuePathInfo.ownerName) continue; const el = document.createElement('div'); el.classList.add('tw-p-3'); @@ -38,7 +36,7 @@ export function attachRefIssueContextPopup(refIssues) { role: 'dialog', interactiveBorder: 5, onShow: () => { - el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}})); + el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: issuePathInfo})); }, }); } diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts index 6a4bfafc924..6227a85e33e 100644 --- a/web_src/js/svg.ts +++ b/web_src/js/svg.ts @@ -153,7 +153,8 @@ export type SvgName = keyof typeof svgs; // most of the SVG icons in assets couldn't be used directly. // retrieve an HTML string for given SVG icon name, size and additional classes -export function svg(name: SvgName, size = 16, className = '') { +export function svg(name: SvgName, size = 16, classNames: string|string[]): string { + const className = Array.isArray(classNames) ? classNames.join(' ') : classNames; if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`); if (size === 16 && !className) return svgs[name]; diff --git a/web_src/js/types.ts b/web_src/js/types.ts index c38c8bda96f..9c601456bd9 100644 --- a/web_src/js/types.ts +++ b/web_src/js/types.ts @@ -30,15 +30,16 @@ export type RequestOpts = { data?: RequestData, } & RequestInit; -export type IssueData = { - owner: string, - repo: string, - type: string, - index: string, +export type IssuePathInfo = { + ownerName: string, + repoName: string, + pathType: string, + indexString?: string, } export type Issue = { id: number; + number: number; title: string; state: 'open' | 'closed'; pull_request?: { diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts index 55896706fff..647676bf205 100644 --- a/web_src/js/utils.test.ts +++ b/web_src/js/utils.test.ts @@ -1,7 +1,7 @@ import { basename, extname, isObject, stripTags, parseIssueHref, parseUrl, translateMonth, translateDay, blobToDataURI, - toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, + toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseIssueNewHref, } from './utils.ts'; test('basename', () => { @@ -28,21 +28,27 @@ test('stripTags', () => { }); test('parseIssueHref', () => { - expect(parseIssueHref('/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('/owner/repo/pulls/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'}); - expect(parseIssueHref('/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('/sub/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('/sub/sub2/owner/repo/pulls/1')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'}); - expect(parseIssueHref('/sub/sub2/owner/repo/issues/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('/sub/sub2/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('https://example.com/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('https://example.com/owner/repo/pulls/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'}); - expect(parseIssueHref('https://example.com/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('https://example.com/sub/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/pulls/1')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '1'}); - expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1#hash')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'}); - expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined}); + expect(parseIssueHref('/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('/owner/repo/pulls/1?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '1'}); + expect(parseIssueHref('/owner/repo/issues/1#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('/sub/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('/sub/sub2/owner/repo/pulls/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '1'}); + expect(parseIssueHref('/sub/sub2/owner/repo/issues/1?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('/sub/sub2/owner/repo/issues/1#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('https://example.com/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('https://example.com/owner/repo/pulls/1?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '1'}); + expect(parseIssueHref('https://example.com/owner/repo/issues/1#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('https://example.com/sub/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/pulls/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls', indexString: '1'}); + expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('https://example.com/sub/sub2/owner/repo/issues/1#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'}); + expect(parseIssueHref('')).toEqual({ownerName: undefined, repoName: undefined, type: undefined, index: undefined}); +}); + +test('parseIssueNewHref', () => { + expect(parseIssueNewHref('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); + expect(parseIssueNewHref('/owner/repo/issues/new?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); + expect(parseIssueNewHref('/sub/owner/repo/issues/new#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); }); test('parseUrl', () => { diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index c52bf500d43..066a7c7b546 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -1,5 +1,5 @@ import {encode, decode} from 'uint8-to-base64'; -import type {IssueData} from './types.ts'; +import type {IssuePathInfo} from './types.ts'; // transform /path/to/file.ext to file.ext export function basename(path: string): string { @@ -31,10 +31,16 @@ export function stripTags(text: string): string { return text.replace(/<[^>]*>?/g, ''); } -export function parseIssueHref(href: string): IssueData { +export function parseIssueHref(href: string): IssuePathInfo { const path = (href || '').replace(/[#?].*$/, ''); - const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || []; - return {owner, repo, type, index}; + const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || []; + return {ownerName, repoName, pathType, indexString}; +} + +export function parseIssueNewHref(href: string): IssuePathInfo { + const path = (href || '').replace(/[#?].*$/, ''); + const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/new/.exec(path) || []; + return {ownerName, repoName, pathType, indexString}; } // parse a URL, either relative '/path' or absolute 'https://localhost/path' diff --git a/web_src/js/utils/dom.test.ts b/web_src/js/utils/dom.test.ts index 13df82d9b49..5c235795fd4 100644 --- a/web_src/js/utils/dom.test.ts +++ b/web_src/js/utils/dom.test.ts @@ -8,11 +8,11 @@ test('createElementFromAttrs', () => { const el = createElementFromAttrs('button', { id: 'the-id', class: 'cls-1 cls-2', - 'data-foo': 'the-data', disabled: true, checked: false, required: null, tabindex: 0, - }); - expect(el.outerHTML).toEqual(''); + 'data-foo': 'the-data', + }, 'txt', createElementFromHTML('inner')); + expect(el.outerHTML).toEqual(''); }); diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 7dd63ecbbf0..5b118b991d5 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -298,22 +298,24 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st } // Warning: Do not enter any unsanitized variables here -export function createElementFromHTML(htmlString: string) { +export function createElementFromHTML(htmlString: string): HTMLElement { const div = document.createElement('div'); div.innerHTML = htmlString.trim(); - return div.firstChild as Element; + return div.firstChild as HTMLElement; } -export function createElementFromAttrs(tagName: string, attrs: Record) { +export function createElementFromAttrs(tagName: string, attrs: Record, ...children: (Node|string)[]): HTMLElement { const el = document.createElement(tagName); - for (const [key, value] of Object.entries(attrs)) { + for (const [key, value] of Object.entries(attrs || {})) { if (value === undefined || value === null) continue; if (typeof value === 'boolean') { el.toggleAttribute(key, value); } else { el.setAttribute(key, String(value)); } - // TODO: in the future we could make it also support "textContent" and "innerHTML" properties if needed + } + for (const child of children) { + el.append(child instanceof Node ? child : document.createTextNode(child)); } return el; } diff --git a/web_src/js/utils/match.ts b/web_src/js/utils/match.ts index 2c7271f16ee..1161ea6250b 100644 --- a/web_src/js/utils/match.ts +++ b/web_src/js/utils/match.ts @@ -1,6 +1,6 @@ import emojis from '../../../assets/emoji.json'; -import type {Issue} from '../features/issue.ts'; import {GET} from '../modules/fetch.ts'; +import type {Issue} from '../features/issue.ts'; const maxMatches = 6; @@ -49,8 +49,8 @@ export async function matchIssue(owner: string, repo: string, issueIndexStr: str const res = await GET(`${window.config.appSubUrl}/${owner}/${repo}/issues/suggestions?q=${encodeURIComponent(query)}`); const issues: Issue[] = await res.json(); - const issueIndex = parseInt(issueIndexStr); + const issueNumber = parseInt(issueIndexStr); // filter out issue with same id - return issues.filter((i) => i.id !== issueIndex); + return issues.filter((i) => i.number !== issueNumber); }