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/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/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
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" .}}
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);
}
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) {