mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-02 17:20:53 +02:00
let user choose SSH key owner when migrating to an org
This commit is contained in:
1 parent
781666b5eb
commit
6ce30aa144
13 files changed
+109
-19
No files matched your search
@@ -17,6 +17,9 @@ type MigrateOptions struct {
|
|||||||
AuthPasswordEncrypted string `json:"auth_password_encrypted,omitempty"`
|
AuthPasswordEncrypted string `json:"auth_password_encrypted,omitempty"`
|
||||||
AuthToken string `json:"-"`
|
AuthToken string `json:"-"`
|
||||||
AuthTokenEncrypted string `json:"auth_token_encrypted,omitempty"`
|
AuthTokenEncrypted string `json:"auth_token_encrypted,omitempty"`
|
||||||
|
// SSHKeyOwnerID overrides the owner whose managed SSH keypair authenticates
|
||||||
|
// the SSH clone. 0 means use the target repository owner (the default).
|
||||||
|
SSHKeyOwnerID int64 `json:"ssh_key_owner_id,omitempty"`
|
||||||
// required: true
|
// required: true
|
||||||
UID int `json:"uid" binding:"Required"`
|
UID int `json:"uid" binding:"Required"`
|
||||||
// required: true
|
// required: true
|
||||||
|
|||||||
+14
-6
@@ -5,13 +5,14 @@ package ssh
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitea.dev/models/db"
|
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
giturl "gitea.dev/modules/git/url"
|
giturl "gitea.dev/modules/git/url"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsSSHURL checks if a URL is an SSH URL
|
// IsSSHURL checks if a URL is an SSH URL
|
||||||
@@ -25,7 +26,7 @@ func IsSSHURL(remote string) bool {
|
|||||||
func GetOrCreateSSHKeypair(ctx context.Context, ownerID int64) (*user_model.SSHKeypair, error) {
|
func GetOrCreateSSHKeypair(ctx context.Context, ownerID int64) (*user_model.SSHKeypair, error) {
|
||||||
keypair, err := user_model.GetSSHKeypairByOwner(ctx, ownerID)
|
keypair, err := user_model.GetSSHKeypairByOwner(ctx, ownerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if db.IsErrNotExist(err) {
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
log.Debug("Creating new SSH keypair for owner %d", ownerID)
|
log.Debug("Creating new SSH keypair for owner %d", ownerID)
|
||||||
return user_model.CreateSSHKeypair(ctx, ownerID)
|
return user_model.CreateSSHKeypair(ctx, ownerID)
|
||||||
}
|
}
|
||||||
@@ -43,15 +44,22 @@ func GetSSHKeypairForRepository(ctx context.Context, repo *repo_model.Repository
|
|||||||
// migration git operation against remoteURL on behalf of repo. For non-SSH
|
// migration git operation against remoteURL on behalf of repo. For non-SSH
|
||||||
// URLs (or when no keypair is available) it is a no-op. The returned cleanup
|
// URLs (or when no keypair is available) it is a no-op. The returned cleanup
|
||||||
// is never nil and must always be called by the caller (typically via defer).
|
// is never nil and must always be called by the caller (typically via defer).
|
||||||
func SetupManagedSSHAgent(ctx context.Context, repo *repo_model.Repository, remoteURL string) (sshAuthSock string, cleanup func(), err error) {
|
// If sshKeyOwnerID is non-zero, the keypair of that owner is used instead of
|
||||||
|
// the repository owner's (used when migrating to an org and the user wants
|
||||||
|
// to authenticate with their personal managed key).
|
||||||
|
func SetupManagedSSHAgent(ctx context.Context, repo *repo_model.Repository, remoteURL string, sshKeyOwnerID int64) (sshAuthSock string, cleanup func(), err error) {
|
||||||
noop := func() {}
|
noop := func() {}
|
||||||
if !IsSSHURL(remoteURL) {
|
if !IsSSHURL(remoteURL) {
|
||||||
return "", noop, nil
|
return "", noop, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
keypair, err := GetSSHKeypairForRepository(ctx, repo)
|
ownerID := repo.OwnerID
|
||||||
|
if sshKeyOwnerID != 0 {
|
||||||
|
ownerID = sshKeyOwnerID
|
||||||
|
}
|
||||||
|
keypair, err := GetOrCreateSSHKeypair(ctx, ownerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", noop, fmt.Errorf("failed to get SSH keypair for repository: %w", err)
|
return "", noop, fmt.Errorf("failed to get SSH keypair for owner %d: %w", ownerID, err)
|
||||||
}
|
}
|
||||||
if keypair == nil {
|
if keypair == nil {
|
||||||
return "", noop, nil
|
return "", noop, nil
|
||||||
@@ -67,6 +75,6 @@ func SetupManagedSSHAgent(ctx context.Context, repo *repo_model.Repository, remo
|
|||||||
return "", noop, fmt.Errorf("failed to create SSH agent: %w", err)
|
return "", noop, fmt.Errorf("failed to create SSH agent: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("SSH agent ready for mirror %s (socket: %s)", repo.FullName(), socketPath)
|
log.Debug("SSH agent ready for %s (socket: %s)", repo.FullName(), socketPath)
|
||||||
return socketPath, agentCleanup, nil
|
return socketPath, agentCleanup, nil
|
||||||
}
|
}
|
||||||
@@ -404,6 +404,9 @@ type MigrateRepoOptions struct {
|
|||||||
AuthUsername string `json:"auth_username"`
|
AuthUsername string `json:"auth_username"`
|
||||||
AuthPassword string `json:"auth_password"`
|
AuthPassword string `json:"auth_password"`
|
||||||
AuthToken string `json:"auth_token"`
|
AuthToken string `json:"auth_token"`
|
||||||
|
// SSHKeyOwnerID picks which managed SSH keypair to use for SSH clones.
|
||||||
|
// 0 means use the target repo owner (default).
|
||||||
|
SSHKeyOwnerID int64 `json:"ssh_key_owner_id"`
|
||||||
|
|
||||||
Mirror bool `json:"mirror"`
|
Mirror bool `json:"mirror"`
|
||||||
LFS bool `json:"lfs"`
|
LFS bool `json:"lfs"`
|
||||||
|
|||||||
@@ -1122,6 +1122,8 @@
|
|||||||
"repo.migrate.ssh_helper_title": "SSH URLs",
|
"repo.migrate.ssh_helper_title": "SSH URLs",
|
||||||
"repo.migrate.ssh_helper_desc": "Upload your SSH mirror keys to the remote SSH server for authentication.",
|
"repo.migrate.ssh_helper_desc": "Upload your SSH mirror keys to the remote SSH server for authentication.",
|
||||||
"repo.migrate.ssh_helper_link": "View your SSH keys (if migrating to an organization, you may need to upload the organization's SSH keys).",
|
"repo.migrate.ssh_helper_link": "View your SSH keys (if migrating to an organization, you may need to upload the organization's SSH keys).",
|
||||||
|
"repo.migrate.ssh_key_owner_label": "Authenticate with",
|
||||||
|
"repo.migrate.ssh_key_owner_repo_default": "Repository owner's managed SSH key (default)",
|
||||||
"repo.migrate.permission_denied": "You are not allowed to import local repositories.",
|
"repo.migrate.permission_denied": "You are not allowed to import local repositories.",
|
||||||
"repo.migrate.permission_denied_blocked": "You cannot import from disallowed hosts. Please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings.",
|
"repo.migrate.permission_denied_blocked": "You cannot import from disallowed hosts. Please ask the admin to check ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS settings.",
|
||||||
"repo.migrate.invalid_local_path": "The local path is invalid. It doesn't exist or is not a directory.",
|
"repo.migrate.invalid_local_path": "The local path is invalid. It doesn't exist or is not a directory.",
|
||||||
@@ -2792,7 +2794,7 @@
|
|||||||
"org.settings.rename_success": "Organization %[1]s has been renamed to %[2]s successfully.",
|
"org.settings.rename_success": "Organization %[1]s has been renamed to %[2]s successfully.",
|
||||||
"org.settings.rename_no_change": "Organization name is not changed.",
|
"org.settings.rename_no_change": "Organization name is not changed.",
|
||||||
"org.settings.rename_new_org_name": "New Organization Name",
|
"org.settings.rename_new_org_name": "New Organization Name",
|
||||||
"org.settings.ssh_keys": "SSH Mirror Keys",
|
"org.settings.ssh_keys": "Managed SSH Keys",
|
||||||
"org.settings.rename_failed": "Renaming organization failed because of an internal error",
|
"org.settings.rename_failed": "Renaming organization failed because of an internal error",
|
||||||
"org.settings.rename_notices_1": "This operation <strong>CANNOT</strong> be undone.",
|
"org.settings.rename_notices_1": "This operation <strong>CANNOT</strong> be undone.",
|
||||||
"org.settings.rename_notices_2": "The old name will redirect until it is claimed.",
|
"org.settings.rename_notices_2": "The old name will redirect until it is claimed.",
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ func Migrate(ctx *context.APIContext) {
|
|||||||
AuthUsername: form.AuthUsername,
|
AuthUsername: form.AuthUsername,
|
||||||
AuthPassword: form.AuthPassword,
|
AuthPassword: form.AuthPassword,
|
||||||
AuthToken: form.AuthToken,
|
AuthToken: form.AuthToken,
|
||||||
|
SSHKeyOwnerID: form.SSHKeyOwnerID,
|
||||||
Wiki: form.Wiki,
|
Wiki: form.Wiki,
|
||||||
Issues: form.Issues,
|
Issues: form.Issues,
|
||||||
Milestones: form.Milestones,
|
Milestones: form.Milestones,
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ func MigratePost(ctx *context.Context) {
|
|||||||
AuthUsername: form.AuthUsername,
|
AuthUsername: form.AuthUsername,
|
||||||
AuthPassword: form.AuthPassword,
|
AuthPassword: form.AuthPassword,
|
||||||
AuthToken: form.AuthToken,
|
AuthToken: form.AuthToken,
|
||||||
|
SSHKeyOwnerID: form.SSHKeyOwnerID,
|
||||||
Wiki: form.Wiki,
|
Wiki: form.Wiki,
|
||||||
Issues: form.Issues,
|
Issues: form.Issues,
|
||||||
Milestones: form.Milestones,
|
Milestones: form.Milestones,
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ type MigrateRepoForm struct {
|
|||||||
AuthUsername string `json:"auth_username"`
|
AuthUsername string `json:"auth_username"`
|
||||||
AuthPassword string `json:"auth_password"`
|
AuthPassword string `json:"auth_password"`
|
||||||
AuthToken string `json:"auth_token"`
|
AuthToken string `json:"auth_token"`
|
||||||
|
// SSHKeyOwnerID picks which managed SSH keypair to use for SSH clones.
|
||||||
|
// 0 means use the target repo owner (default).
|
||||||
|
SSHKeyOwnerID int64 `json:"ssh_key_owner_id"`
|
||||||
// required: true
|
// required: true
|
||||||
UID int64 `json:"uid" binding:"Required"`
|
UID int64 `json:"uid" binding:"Required"`
|
||||||
// required: true
|
// required: true
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ func (g *GiteaLocalUploader) CreateRepo(ctx context.Context, repo *base.Reposito
|
|||||||
Wiki: opts.Wiki,
|
Wiki: opts.Wiki,
|
||||||
Releases: opts.Releases, // if didn't get releases, then sync them from tags
|
Releases: opts.Releases, // if didn't get releases, then sync them from tags
|
||||||
MirrorInterval: opts.MirrorInterval,
|
MirrorInterval: opts.MirrorInterval,
|
||||||
|
SSHKeyOwnerID: opts.SSHKeyOwnerID,
|
||||||
}, NewMigrationHTTPTransport())
|
}, NewMigrationHTTPTransport())
|
||||||
|
|
||||||
g.sameApp = strings.HasPrefix(repo.OriginalURL, setting.AppURL)
|
g.sameApp = strings.HasPrefix(repo.OriginalURL, setting.AppURL)
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*repo_module.SyncResu
|
|||||||
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
|
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
|
||||||
|
|
||||||
// Setup SSH authentication if needed
|
// Setup SSH authentication if needed
|
||||||
sshAuthSock, cleanup, sshErr := ssh_module.SetupManagedSSHAgent(ctx, m.Repo, remoteURL.String())
|
sshAuthSock, cleanup, sshErr := ssh_module.SetupManagedSSHAgent(ctx, m.Repo, remoteURL.String(), 0)
|
||||||
if sshErr != nil {
|
if sshErr != nil {
|
||||||
log.Error("SyncMirrors [repo: %-v]: SSH setup error %v", m.Repo, sshErr)
|
log.Error("SyncMirrors [repo: %-v]: SSH setup error %v", m.Repo, sshErr)
|
||||||
return nil, false
|
return nil, false
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup SSH authentication
|
// Setup SSH authentication
|
||||||
sshAuthSock, cleanup, err := ssh_module.SetupManagedSSHAgent(ctx, repo, remoteURL.String())
|
sshAuthSock, cleanup, err := ssh_module.SetupManagedSSHAgent(ctx, repo, remoteURL.String(), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to set up SSH agent for push mirror %s: %v", repo.FullName(), err)
|
log.Error("Failed to set up SSH agent for push mirror %s: %v", repo.FullName(), err)
|
||||||
return util.SanitizeErrorCredentialURLs(err)
|
return util.SanitizeErrorCredentialURLs(err)
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ import (
|
|||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func cloneExternalRepoWithSSHAuth(ctx context.Context, repo *repo_model.Repository, remoteURL string, storageRepo gitrepo.Repository, cloneOpts git.CloneRepoOptions) error {
|
func cloneExternalRepoWithSSHAuth(ctx context.Context, repo *repo_model.Repository, remoteURL string, storageRepo gitrepo.Repository, cloneOpts git.CloneRepoOptions, sshKeyOwnerID int64) error {
|
||||||
sshAuthSock, cleanup, err := ssh_module.SetupManagedSSHAgent(ctx, repo, remoteURL)
|
sshAuthSock, cleanup, err := ssh_module.SetupManagedSSHAgent(ctx, repo, remoteURL, sshKeyOwnerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration.
|
|||||||
SkipTLSVerify: setting.Migrations.SkipTLSVerify,
|
SkipTLSVerify: setting.Migrations.SkipTLSVerify,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cloneExternalRepoWithSSHAuth(ctx, repo, wikiRemoteURL, storageRepo, cloneOpts); err != nil {
|
if err := cloneExternalRepoWithSSHAuth(ctx, repo, wikiRemoteURL, storageRepo, cloneOpts, opts.SSHKeyOwnerID); err != nil {
|
||||||
log.Error("Clone wiki failed, err: %v", err)
|
log.Error("Clone wiki failed, err: %v", err)
|
||||||
cleanIncompleteWikiPath()
|
cleanIncompleteWikiPath()
|
||||||
return "", err
|
return "", err
|
||||||
@@ -115,7 +115,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
|||||||
repo.Owner = u
|
repo.Owner = u
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cloneExternalRepoWithSSHAuth(ctx, repo, opts.CloneAddr, repo, cloneOpts); err != nil {
|
if err := cloneExternalRepoWithSSHAuth(ctx, repo, opts.CloneAddr, repo, cloneOpts, opts.SSHKeyOwnerID); err != nil {
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
return repo, fmt.Errorf("clone timed out, consider increasing [git.timeout] MIGRATE in app.ini, underlying err: %w", err)
|
return repo, fmt.Errorf("clone timed out, consider increasing [git.timeout] MIGRATE in app.ini, underlying err: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,13 @@
|
|||||||
<label for="auth_password">{{ctx.Locale.Tr "password"}}</label>
|
<label for="auth_password">{{ctx.Locale.Tr "password"}}</label>
|
||||||
<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}">
|
<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="inline field ssh-key-owner-selector tw-hidden" data-signed-user-id="{{.SignedUser.ID}}" data-signed-user-name="{{.SignedUser.Name}}">
|
||||||
|
<label for="ssh_key_owner_select">{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_label"}}</label>
|
||||||
|
<select id="ssh_key_owner_select" name="ssh_key_owner_id_select">
|
||||||
|
<option value="0">{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_repo_default"}}</option>
|
||||||
|
</select>
|
||||||
|
<input id="ssh_key_owner_id" name="ssh_key_owner_id" type="hidden" value="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
{{template "repo/migrate/options" .}}
|
{{template "repo/migrate/options" .}}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ async function doMigrationRetry(e: Event) {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSSHURL(url: string): boolean {
|
||||||
|
return url.startsWith('ssh://') ||
|
||||||
|
url.startsWith('git@') ||
|
||||||
|
(url.includes('@') && url.includes(':') && !url.includes('://'));
|
||||||
|
}
|
||||||
|
|
||||||
export function initRepoMigrationForm() {
|
export function initRepoMigrationForm() {
|
||||||
const cloneAddrInput = document.querySelector<HTMLInputElement>('#clone_addr');
|
const cloneAddrInput = document.querySelector<HTMLInputElement>('#clone_addr');
|
||||||
const authUsernameInput = document.querySelector<HTMLInputElement>('#auth_username');
|
const authUsernameInput = document.querySelector<HTMLInputElement>('#auth_username');
|
||||||
@@ -68,12 +74,6 @@ export function initRepoMigrationForm() {
|
|||||||
|
|
||||||
if (!cloneAddrInput || !authUsernameInput || !authPasswordInput || !sshHelpText) return;
|
if (!cloneAddrInput || !authUsernameInput || !authPasswordInput || !sshHelpText) return;
|
||||||
|
|
||||||
function isSSHURL(url: string): boolean {
|
|
||||||
return url.startsWith('ssh://') ||
|
|
||||||
url.startsWith('git@') ||
|
|
||||||
(url.includes('@') && url.includes(':') && !url.includes('://'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAuthFields() {
|
function updateAuthFields() {
|
||||||
const url = cloneAddrInput!.value.trim();
|
const url = cloneAddrInput!.value.trim();
|
||||||
const isSSH = isSSHURL(url);
|
const isSSH = isSSHURL(url);
|
||||||
@@ -99,4 +99,65 @@ export function initRepoMigrationForm() {
|
|||||||
updateAuthFields();
|
updateAuthFields();
|
||||||
cloneAddrInput.addEventListener('input', updateAuthFields);
|
cloneAddrInput.addEventListener('input', updateAuthFields);
|
||||||
cloneAddrInput.addEventListener('blur', updateAuthFields);
|
cloneAddrInput.addEventListener('blur', updateAuthFields);
|
||||||
|
|
||||||
|
initSSHKeyOwnerSelector(cloneAddrInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// initSSHKeyOwnerSelector wires the "managed SSH key owner" selector. It is
|
||||||
|
// hidden by default and only shown when an SSH URL is entered AND the chosen
|
||||||
|
// target owner is an organisation (i.e. not the signed-in user) — in that case
|
||||||
|
// the user can pick between the org's managed key (default) and their personal
|
||||||
|
// managed key. The hidden #ssh_key_owner_id field is submitted with the form.
|
||||||
|
function initSSHKeyOwnerSelector(cloneAddrInput: HTMLInputElement) {
|
||||||
|
const container = document.querySelector<HTMLElement>('.ssh-key-owner-selector');
|
||||||
|
const select = document.querySelector<HTMLSelectElement>('#ssh_key_owner_select');
|
||||||
|
const hiddenId = document.querySelector<HTMLInputElement>('#ssh_key_owner_id');
|
||||||
|
const uidInput = document.querySelector<HTMLInputElement>('#uid');
|
||||||
|
if (!container || !select || !hiddenId || !uidInput) return;
|
||||||
|
|
||||||
|
const signedUserID = container.dataset.signedUserId ?? '';
|
||||||
|
const signedUserName = container.dataset.signedUserName ?? '';
|
||||||
|
|
||||||
|
// Build {ownerID -> name} from the owner dropdown menu items
|
||||||
|
const ownerNameById = new Map<string, string>();
|
||||||
|
for (const item of document.querySelectorAll<HTMLElement>('.owner.dropdown .menu .item')) {
|
||||||
|
const id = item.dataset.value;
|
||||||
|
const name = item.getAttribute('title') ?? item.textContent?.trim() ?? '';
|
||||||
|
if (id) ownerNameById.set(id, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const isSSH = isSSHURL(cloneAddrInput.value.trim());
|
||||||
|
const targetUid = uidInput!.value;
|
||||||
|
|
||||||
|
// No choice: non-SSH URL, or migrating into the user's own account
|
||||||
|
if (!isSSH || targetUid === signedUserID) {
|
||||||
|
hideElem(container!);
|
||||||
|
hiddenId!.value = '0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target is an organisation — offer both keys
|
||||||
|
const orgName = ownerNameById.get(targetUid) ?? `#${targetUid}`;
|
||||||
|
select!.innerHTML = '';
|
||||||
|
select!.add(new Option(`Use ${orgName}'s managed SSH key (default)`, '0'));
|
||||||
|
select!.add(new Option(`Use your personal managed SSH key (${signedUserName})`, signedUserID));
|
||||||
|
select!.value = hiddenId!.value || '0';
|
||||||
|
hiddenId!.value = select!.value;
|
||||||
|
showElem(container!);
|
||||||
|
}
|
||||||
|
|
||||||
|
select.addEventListener('change', () => {
|
||||||
|
hiddenId!.value = select!.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Semantic UI updates the #uid hidden input via menu item clicks
|
||||||
|
for (const item of document.querySelectorAll<HTMLElement>('.owner.dropdown .menu .item')) {
|
||||||
|
item.addEventListener('click', () => setTimeout(update, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneAddrInput.addEventListener('input', update);
|
||||||
|
cloneAddrInput.addEventListener('blur', update);
|
||||||
|
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user