mirror of
https://github.com/go-gitea/gitea
synced 2025-02-24 13:11:25 +01:00
Merge branch 'main' into lunny/fix_edit_team
This commit is contained in:
commit
d74263430f
@ -53,8 +53,6 @@ func (e Emoji) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var err error
|
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// generate data
|
// generate data
|
||||||
@ -83,8 +81,6 @@ var replacer = strings.NewReplacer(
|
|||||||
var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`)
|
var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`)
|
||||||
|
|
||||||
func generate() ([]byte, error) {
|
func generate() ([]byte, error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
// load gemoji data
|
// load gemoji data
|
||||||
res, err := http.Get(gemojiURL)
|
res, err := http.Get(gemojiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -136,8 +136,6 @@ var ErrLFSObjectNotExist = db.ErrNotExist{Resource: "LFS Meta object"}
|
|||||||
// NewLFSMetaObject stores a given populated LFSMetaObject structure in the database
|
// NewLFSMetaObject stores a given populated LFSMetaObject structure in the database
|
||||||
// if it is not already present.
|
// if it is not already present.
|
||||||
func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMetaObject, error) {
|
func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMetaObject, error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -229,8 +229,7 @@ func TestGetLabelsByOrgID(t *testing.T) {
|
|||||||
testSuccess(3, "reversealphabetically", []int64{4, 3})
|
testSuccess(3, "reversealphabetically", []int64{4, 3})
|
||||||
testSuccess(3, "default", []int64{3, 4})
|
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))
|
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||||
|
|
||||||
_, err = issues_model.GetLabelsByOrgID(db.DefaultContext, -1, "leastissues", db.ListOptions{})
|
_, err = issues_model.GetLabelsByOrgID(db.DefaultContext, -1, "leastissues", db.ListOptions{})
|
||||||
|
@ -40,14 +40,12 @@ func TestMaybeRemoveBOM(t *testing.T) {
|
|||||||
|
|
||||||
func TestToUTF8(t *testing.T) {
|
func TestToUTF8(t *testing.T) {
|
||||||
resetDefaultCharsetsOrder()
|
resetDefaultCharsetsOrder()
|
||||||
var res string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Note: golang compiler seems so behave differently depending on the current
|
// Note: golang compiler seems so behave differently depending on the current
|
||||||
// locale, so some conversions might behave differently. For that reason, we don't
|
// locale, so some conversions might behave differently. For that reason, we don't
|
||||||
// depend on particular conversions but in expected behaviors.
|
// 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.NoError(t, err)
|
||||||
assert.Equal(t, "ABC", res)
|
assert.Equal(t, "ABC", res)
|
||||||
|
|
||||||
|
@ -213,8 +213,7 @@ func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node
|
|||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
_, err := w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
|
||||||
_, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ast.WalkStop, err
|
return ast.WalkStop, err
|
||||||
}
|
}
|
||||||
|
@ -202,7 +202,6 @@ func Search(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusInternalServerError, api.SearchError{
|
ctx.JSON(http.StatusInternalServerError, api.SearchError{
|
||||||
|
@ -111,7 +111,6 @@ func ListMyRepos(ctx *context.APIContext) {
|
|||||||
IncludeDescription: true,
|
IncludeDescription: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
repos, count, err := repo_model.SearchRepository(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "SearchRepository", err)
|
ctx.Error(http.StatusInternalServerError, "SearchRepository", err)
|
||||||
|
@ -276,7 +276,6 @@ func ViewPost(ctx *context_module.Context) {
|
|||||||
if validCursor {
|
if validCursor {
|
||||||
length := step.LogLength - cursor.Cursor
|
length := step.LogLength - cursor.Cursor
|
||||||
offset := task.LogIndexes[index]
|
offset := task.LogIndexes[index]
|
||||||
var err error
|
|
||||||
logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
|
logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
|
@ -94,7 +94,6 @@ func ActivityAuthors(ctx *context.Context) {
|
|||||||
timeFrom = timeUntil.Add(-time.Hour * 168)
|
timeFrom = timeUntil.Add(-time.Hour * 168)
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
authors, err := activities_model.GetActivityStatsTopAuthors(ctx, ctx.Repo.Repository, timeFrom, 10)
|
authors, err := activities_model.GetActivityStatsTopAuthors(ctx, ctx.Repo.Repository, timeFrom, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetActivityStatsTopAuthors", err)
|
ctx.ServerError("GetActivityStatsTopAuthors", err)
|
||||||
|
@ -11,19 +11,10 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
|
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/services/context"
|
"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
|
// IssueSuggestions returns a list of issue suggestions
|
||||||
func IssueSuggestions(ctx *context.Context) {
|
func IssueSuggestions(ctx *context.Context) {
|
||||||
keyword := ctx.Req.FormValue("q")
|
keyword := ctx.Req.FormValue("q")
|
||||||
@ -61,13 +52,14 @@ func IssueSuggestions(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
suggestions := make([]*issueSuggestion, 0, len(issues))
|
suggestions := make([]*structs.Issue, 0, len(issues))
|
||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
suggestion := &issueSuggestion{
|
suggestion := &structs.Issue{
|
||||||
ID: issue.ID,
|
ID: issue.ID,
|
||||||
|
Index: issue.Index,
|
||||||
Title: issue.Title,
|
Title: issue.Title,
|
||||||
State: string(issue.State()),
|
State: issue.State(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue.IsPull {
|
if issue.IsPull {
|
||||||
@ -76,12 +68,9 @@ func IssueSuggestions(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if issue.PullRequest != nil {
|
if issue.PullRequest != nil {
|
||||||
suggestion.PullRequest = &struct {
|
suggestion.PullRequest = &structs.PullRequestMeta{
|
||||||
Merged bool `json:"merged"`
|
HasMerged: issue.PullRequest.HasMerged,
|
||||||
Draft bool `json:"draft"`
|
IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
|
||||||
}{
|
|
||||||
Merged: issue.PullRequest.HasMerged,
|
|
||||||
Draft: issue.PullRequest.IsWorkInProgress(ctx),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,6 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
|
|||||||
// this should be impossible; if subTreeEntry exists so should this.
|
// this should be impossible; if subTreeEntry exists so should this.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var err error
|
|
||||||
childEntries, err := subTree.ListEntries()
|
childEntries, err := subTree.ListEntries()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
|
@ -52,8 +52,6 @@ func GetTemplateConfig(gitRepo *git.Repository, path string, commit *git.Commit)
|
|||||||
return GetDefaultTemplateConfig(), nil
|
return GetDefaultTemplateConfig(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
treeEntry, err := commit.GetTreeEntryByPath(path)
|
treeEntry, err := commit.GetTreeEntryByPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return GetDefaultTemplateConfig(), err
|
return GetDefaultTemplateConfig(), err
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
{{template "base/head" .}}
|
{{template "base/head" .}}
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
|
<div role="main" aria-label="{{.Title}}" class="page-content organization repository projects view-project">
|
||||||
{{template "shared/user/org_profile_avatar" .}}
|
{{if .ContextUser.IsOrganization}}
|
||||||
<div class="ui container tw-mb-4">
|
{{template "org/header" .}}
|
||||||
{{template "user/overview/header" .}}
|
{{else}}
|
||||||
</div>
|
{{template "shared/user/org_profile_avatar" .}}
|
||||||
|
<div class="ui container tw-mb-4">
|
||||||
|
{{template "user/overview/header" .}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<div class="ui container fluid padded">
|
<div class="ui container fluid padded">
|
||||||
{{template "projects/view" .}}
|
{{template "projects/view" .}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@ import {SvgIcon} from '../svg.ts';
|
|||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
import {getIssueColor, getIssueIcon} from '../features/issue.ts';
|
import {getIssueColor, getIssueIcon} from '../features/issue.ts';
|
||||||
import {computed, onMounted, ref} from 'vue';
|
import {computed, onMounted, ref} from 'vue';
|
||||||
|
import type {IssuePathInfo} from '../types.ts';
|
||||||
|
|
||||||
const {appSubUrl, i18n} = window.config;
|
const {appSubUrl, i18n} = window.config;
|
||||||
|
|
||||||
@ -25,19 +26,19 @@ const root = ref<HTMLElement | null>(null);
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => {
|
root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => {
|
||||||
const data = e.detail;
|
const data: IssuePathInfo = e.detail;
|
||||||
if (!loading.value && issue.value === null) {
|
if (!loading.value && issue.value === null) {
|
||||||
load(data);
|
load(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function load(data) {
|
async function load(issuePathInfo: IssuePathInfo) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
i18nErrorMessage.value = null;
|
i18nErrorMessage.value = null;
|
||||||
|
|
||||||
try {
|
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();
|
const respJson = await response.json();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
i18nErrorMessage.value = respJson.message ?? i18n.network_error;
|
i18nErrorMessage.value = respJson.message ?? i18n.network_error;
|
||||||
|
@ -1,39 +1,29 @@
|
|||||||
import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts';
|
import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts';
|
||||||
import {emojiString} from '../emoji.ts';
|
import {emojiString} from '../emoji.ts';
|
||||||
import {svg} from '../../svg.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 {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts';
|
||||||
import {getIssueColor, getIssueIcon} from '../issue.ts';
|
import {getIssueColor, getIssueIcon} from '../issue.ts';
|
||||||
import {debounce} from 'perfect-debounce';
|
import {debounce} from 'perfect-debounce';
|
||||||
|
|
||||||
const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => {
|
const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => {
|
||||||
const {owner, repo, index} = parseIssueHref(window.location.href);
|
let issuePathInfo = parseIssueHref(window.location.href);
|
||||||
const matches = await matchIssue(owner, repo, index, text);
|
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});
|
if (!matches.length) return resolve({matched: false});
|
||||||
|
|
||||||
const ul = document.createElement('ul');
|
const ul = createElementFromAttrs('ul', {class: 'suggestions'});
|
||||||
ul.classList.add('suggestions');
|
|
||||||
for (const issue of matches) {
|
for (const issue of matches) {
|
||||||
const li = createElementFromAttrs('li', {
|
const li = createElementFromAttrs(
|
||||||
role: 'option',
|
'li', {role: 'option', class: 'tw-flex tw-gap-2', 'data-value': `${key}${issue.number}`},
|
||||||
'data-value': `${key}${issue.id}`,
|
createElementFromHTML(svg(getIssueIcon(issue), 16, ['text', getIssueColor(issue)])),
|
||||||
class: 'tw-flex tw-gap-2',
|
createElementFromAttrs('span', null, `#${issue.number}`),
|
||||||
});
|
createElementFromAttrs('span', null, issue.title),
|
||||||
|
);
|
||||||
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);
|
|
||||||
|
|
||||||
ul.append(li);
|
ul.append(li);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve({matched: true, fragment: ul});
|
resolve({matched: true, fragment: ul});
|
||||||
}), 100);
|
}), 100);
|
||||||
|
|
||||||
|
@ -10,12 +10,10 @@ export function initContextPopups() {
|
|||||||
|
|
||||||
export function attachRefIssueContextPopup(refIssues) {
|
export function attachRefIssueContextPopup(refIssues) {
|
||||||
for (const refIssue of refIssues) {
|
for (const refIssue of refIssues) {
|
||||||
if (refIssue.classList.contains('ref-external-issue')) {
|
if (refIssue.classList.contains('ref-external-issue')) continue;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {owner, repo, index} = parseIssueHref(refIssue.getAttribute('href'));
|
const issuePathInfo = parseIssueHref(refIssue.getAttribute('href'));
|
||||||
if (!owner) return;
|
if (!issuePathInfo.ownerName) continue;
|
||||||
|
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.classList.add('tw-p-3');
|
el.classList.add('tw-p-3');
|
||||||
@ -38,7 +36,7 @@ export function attachRefIssueContextPopup(refIssues) {
|
|||||||
role: 'dialog',
|
role: 'dialog',
|
||||||
interactiveBorder: 5,
|
interactiveBorder: 5,
|
||||||
onShow: () => {
|
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}));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,8 @@ export type SvgName = keyof typeof svgs;
|
|||||||
// most of the SVG icons in assets couldn't be used directly.
|
// 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
|
// 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 (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`);
|
||||||
if (size === 16 && !className) return svgs[name];
|
if (size === 16 && !className) return svgs[name];
|
||||||
|
|
||||||
|
@ -30,15 +30,16 @@ export type RequestOpts = {
|
|||||||
data?: RequestData,
|
data?: RequestData,
|
||||||
} & RequestInit;
|
} & RequestInit;
|
||||||
|
|
||||||
export type IssueData = {
|
export type IssuePathInfo = {
|
||||||
owner: string,
|
ownerName: string,
|
||||||
repo: string,
|
repoName: string,
|
||||||
type: string,
|
pathType: string,
|
||||||
index: string,
|
indexString?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Issue = {
|
export type Issue = {
|
||||||
id: number;
|
id: number;
|
||||||
|
number: number;
|
||||||
title: string;
|
title: string;
|
||||||
state: 'open' | 'closed';
|
state: 'open' | 'closed';
|
||||||
pull_request?: {
|
pull_request?: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
basename, extname, isObject, stripTags, parseIssueHref,
|
basename, extname, isObject, stripTags, parseIssueHref,
|
||||||
parseUrl, translateMonth, translateDay, blobToDataURI,
|
parseUrl, translateMonth, translateDay, blobToDataURI,
|
||||||
toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile,
|
toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseIssueNewHref,
|
||||||
} from './utils.ts';
|
} from './utils.ts';
|
||||||
|
|
||||||
test('basename', () => {
|
test('basename', () => {
|
||||||
@ -28,21 +28,27 @@ test('stripTags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('parseIssueHref', () => {
|
test('parseIssueHref', () => {
|
||||||
expect(parseIssueHref('/owner/repo/issues/1')).toEqual({owner: 'owner', repo: 'repo', type: 'issues', index: '1'});
|
expect(parseIssueHref('/owner/repo/issues/1')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues', indexString: '1'});
|
||||||
expect(parseIssueHref('/owner/repo/pulls/1?query')).toEqual({owner: 'owner', repo: 'repo', type: 'pulls', index: '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({owner: 'owner', repo: 'repo', type: 'issues', index: '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({owner: 'owner', repo: 'repo', type: 'issues', index: '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({owner: 'owner', repo: 'repo', type: 'pulls', index: '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({owner: 'owner', repo: 'repo', type: 'issues', index: '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({owner: 'owner', repo: 'repo', type: 'issues', index: '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({owner: 'owner', repo: 'repo', type: 'issues', index: '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({owner: 'owner', repo: 'repo', type: 'pulls', index: '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({owner: 'owner', repo: 'repo', type: 'issues', index: '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({owner: 'owner', repo: 'repo', type: 'issues', index: '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({owner: 'owner', repo: 'repo', type: 'pulls', index: '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({owner: 'owner', repo: 'repo', type: 'issues', index: '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({owner: 'owner', repo: 'repo', type: 'issues', index: '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({owner: undefined, repo: undefined, type: undefined, index: undefined});
|
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', () => {
|
test('parseUrl', () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {encode, decode} from 'uint8-to-base64';
|
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
|
// transform /path/to/file.ext to file.ext
|
||||||
export function basename(path: string): string {
|
export function basename(path: string): string {
|
||||||
@ -31,10 +31,16 @@ export function stripTags(text: string): string {
|
|||||||
return text.replace(/<[^>]*>?/g, '');
|
return text.replace(/<[^>]*>?/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseIssueHref(href: string): IssueData {
|
export function parseIssueHref(href: string): IssuePathInfo {
|
||||||
const path = (href || '').replace(/[#?].*$/, '');
|
const path = (href || '').replace(/[#?].*$/, '');
|
||||||
const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
|
const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
|
||||||
return {owner, repo, type, index};
|
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'
|
// parse a URL, either relative '/path' or absolute 'https://localhost/path'
|
||||||
|
@ -8,11 +8,11 @@ test('createElementFromAttrs', () => {
|
|||||||
const el = createElementFromAttrs('button', {
|
const el = createElementFromAttrs('button', {
|
||||||
id: 'the-id',
|
id: 'the-id',
|
||||||
class: 'cls-1 cls-2',
|
class: 'cls-1 cls-2',
|
||||||
'data-foo': 'the-data',
|
|
||||||
disabled: true,
|
disabled: true,
|
||||||
checked: false,
|
checked: false,
|
||||||
required: null,
|
required: null,
|
||||||
tabindex: 0,
|
tabindex: 0,
|
||||||
});
|
'data-foo': 'the-data',
|
||||||
expect(el.outerHTML).toEqual('<button id="the-id" class="cls-1 cls-2" data-foo="the-data" disabled="" tabindex="0"></button>');
|
}, 'txt', createElementFromHTML('<span>inner</span>'));
|
||||||
|
expect(el.outerHTML).toEqual('<button id="the-id" class="cls-1 cls-2" disabled="" tabindex="0" data-foo="the-data">txt<span>inner</span></button>');
|
||||||
});
|
});
|
||||||
|
@ -298,22 +298,24 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Warning: Do not enter any unsanitized variables here
|
// Warning: Do not enter any unsanitized variables here
|
||||||
export function createElementFromHTML(htmlString: string) {
|
export function createElementFromHTML(htmlString: string): HTMLElement {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.innerHTML = htmlString.trim();
|
div.innerHTML = htmlString.trim();
|
||||||
return div.firstChild as Element;
|
return div.firstChild as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createElementFromAttrs(tagName: string, attrs: Record<string, any>) {
|
export function createElementFromAttrs(tagName: string, attrs: Record<string, any>, ...children: (Node|string)[]): HTMLElement {
|
||||||
const el = document.createElement(tagName);
|
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 (value === undefined || value === null) continue;
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
el.toggleAttribute(key, value);
|
el.toggleAttribute(key, value);
|
||||||
} else {
|
} else {
|
||||||
el.setAttribute(key, String(value));
|
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;
|
return el;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import emojis from '../../../assets/emoji.json';
|
import emojis from '../../../assets/emoji.json';
|
||||||
import type {Issue} from '../features/issue.ts';
|
|
||||||
import {GET} from '../modules/fetch.ts';
|
import {GET} from '../modules/fetch.ts';
|
||||||
|
import type {Issue} from '../features/issue.ts';
|
||||||
|
|
||||||
const maxMatches = 6;
|
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 res = await GET(`${window.config.appSubUrl}/${owner}/${repo}/issues/suggestions?q=${encodeURIComponent(query)}`);
|
||||||
|
|
||||||
const issues: Issue[] = await res.json();
|
const issues: Issue[] = await res.json();
|
||||||
const issueIndex = parseInt(issueIndexStr);
|
const issueNumber = parseInt(issueIndexStr);
|
||||||
|
|
||||||
// filter out issue with same id
|
// filter out issue with same id
|
||||||
return issues.filter((i) => i.id !== issueIndex);
|
return issues.filter((i) => i.number !== issueNumber);
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,14 @@ test('toAbsoluteLocaleDate', () => {
|
|||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
})).toEqual('15. März 2024');
|
})).toEqual('15. März 2024');
|
||||||
|
|
||||||
expect(toAbsoluteLocaleDate('12345-03-15 01:02:03', '', {
|
// these cases shouldn't happen
|
||||||
year: 'numeric',
|
expect(toAbsoluteLocaleDate('2024-03-15 01:02:03', '', {})).toEqual('Invalid Date');
|
||||||
month: 'short',
|
expect(toAbsoluteLocaleDate('10000-01-01', '', {})).toEqual('Invalid Date');
|
||||||
day: 'numeric',
|
|
||||||
})).toEqual('Mar 15, 12345');
|
// 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;
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
export function toAbsoluteLocaleDate(date: string, lang: string, opts: Intl.DateTimeFormatOptions) {
|
export function toAbsoluteLocaleDate(date: string, lang?: string, opts?: Intl.DateTimeFormatOptions) {
|
||||||
return new Date(date).toLocaleString(lang || [], opts);
|
// 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 {
|
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') ||
|
const lang = this.closest('[lang]')?.getAttribute('lang') ||
|
||||||
this.ownerDocument.documentElement.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'});
|
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) {
|
attributeChangedCallback(_name, oldValue, newValue) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user