diff --git a/include/znc/Config.h b/include/znc/Config.h index dc18ad1b..17783bc7 100644 --- a/include/znc/Config.h +++ b/include/znc/Config.h @@ -35,10 +35,11 @@ struct CConfigEntry { class CConfig { public: - CConfig() : m_ConfigEntries(), m_SubConfigs() {} + CConfig() : m_ConfigEntries(), m_SubConfigs(), m_SubConfigNameSets() {} typedef std::map EntryMap; - typedef std::map SubConfig; + typedef std::pair SubConfigEntry; + typedef std::vector SubConfig; typedef std::map SubConfigMap; typedef EntryMap::const_iterator EntryMapIterator; @@ -62,14 +63,13 @@ class CConfig { bool AddSubConfig(const CString& sTag, const CString& sName, CConfig Config) { - SubConfig& conf = m_SubConfigs[sTag]; - SubConfig::const_iterator it = conf.find(sName); + auto& nameset = m_SubConfigNameSets[sTag]; - if (it != conf.end()) { - return false; - } + if (nameset.find(sName) != nameset.end()) return false; + + nameset.insert(sName); + m_SubConfigs[sTag].emplace_back(sName, Config); - conf[sName] = Config; return true; } @@ -142,9 +142,9 @@ class CConfig { return false; } - bool FindSubConfig(const CString& sName, SubConfig& Config, + bool FindSubConfig(const CString& sTag, SubConfig& Config, bool bErase = true) { - SubConfigMap::iterator it = m_SubConfigs.find(sName); + auto it = m_SubConfigs.find(sTag); if (it == m_SubConfigs.end()) { Config.clear(); return false; @@ -153,6 +153,7 @@ class CConfig { if (bErase) { m_SubConfigs.erase(it); + m_SubConfigNameSets.erase(sTag); } return true; @@ -166,8 +167,12 @@ class CConfig { void Write(CFile& file, unsigned int iIndentation = 0); private: + typedef SCString SubConfigNameSet; + typedef std::map SubConfigNameSetMap; + EntryMap m_ConfigEntries; SubConfigMap m_SubConfigs; + SubConfigNameSetMap m_SubConfigNameSets; }; #endif // !ZNC_CONFIG_H diff --git a/include/znc/IRCNetwork.h b/include/znc/IRCNetwork.h index a9c3deee..6be95631 100644 --- a/include/znc/IRCNetwork.h +++ b/include/znc/IRCNetwork.h @@ -93,6 +93,9 @@ class CIRCNetwork : private CCoreTranslationMixin { bool AddChan(CChan* pChan); bool AddChan(const CString& sName, bool bInConfig); bool DelChan(const CString& sName); + bool MoveChan(const CString& sChan, unsigned int index, CString& sError); + bool SwapChans(const CString& sChan1, const CString& sChan2, + CString& sError); void JoinChans(); void JoinChans(std::set& sChans); diff --git a/modules/data/webadmin/files/webadmin.js b/modules/data/webadmin/files/webadmin.js index ba2ff687..f9774d5d 100644 --- a/modules/data/webadmin/files/webadmin.js +++ b/modules/data/webadmin/files/webadmin.js @@ -165,6 +165,31 @@ function serverlist_init($) { })(); } +function channellist_init($) { + function update_rows() { + $("#channels > tr").each(function(i) { + $(this).toggleClass("evenrow", i % 2 === 1).toggleClass("oddrow", i % 2 === 0); + $(this).find(".channel_index").val(i + 1); + }); + } + $("#channels").sortable({ + axis: "y", + update: update_rows + }); + $(".channel_index").change(function() { + var src = $(this).closest("tr").detach(); + var rows = $("#channels > tr"); + var dst = rows[this.value - 1]; + + if (dst) + src.insertBefore(dst); + else + src.insertAfter(rows.last()); + + update_rows(); + }); +} + function ctcpreplies_init($) { function serialize() { var text = ""; diff --git a/modules/data/webadmin/tmpl/add_edit_network.tmpl b/modules/data/webadmin/tmpl/add_edit_network.tmpl index 93312b57..9a1a9870 100644 --- a/modules/data/webadmin/tmpl/add_edit_network.tmpl +++ b/modules/data/webadmin/tmpl/add_edit_network.tmpl @@ -184,6 +184,7 @@ [] + @@ -196,13 +197,14 @@ - - + + [] [] + checked="checked" /> @@ -213,6 +215,7 @@ + diff --git a/modules/webadmin.cpp b/modules/webadmin.cpp index 7fada328..7671eb8b 100644 --- a/modules/webadmin.cpp +++ b/modules/webadmin.cpp @@ -1001,6 +1001,7 @@ class CWebAdminMod : public CModule { } const vector& Channels = pNetwork->GetChans(); + unsigned int uIndex = 1; for (const CChan* pChan : Channels) { CTemplate& l = Tmpl.AddRow("ChannelLoop"); @@ -1021,6 +1022,9 @@ class CWebAdminMod : public CModule { if (pChan->InConfig()) { l["InConfig"] = "true"; } + + l["MaxIndex"] = CString(Channels.size()); + l["Index"] = CString(uIndex++); } for (const CString& sFP : pNetwork->GetTrustedFingerprints()) { CTemplate& l = Tmpl.AddRow("TrustedFingerprints"); @@ -1157,6 +1161,13 @@ class CWebAdminMod : public CModule { for (const CString& sChan : vsArgs) { CChan* pChan = pNetwork->FindChan(sChan.TrimRight_n("\r")); if (pChan) { + CString sError; + if (!pNetwork->MoveChan( + sChan, WebSock.GetParam("index_" + sChan).ToUInt() - 1, + sError)) { + WebSock.PrintErrorPage(sError); + return true; + } pChan->SetInConfig(WebSock.GetParam("save_" + sChan).ToBool()); } } diff --git a/src/ClientCommand.cpp b/src/ClientCommand.cpp index abfd0531..cd768af0 100644 --- a/src/ClientCommand.cpp +++ b/src/ClientCommand.cpp @@ -453,6 +453,46 @@ void CClient::UserCommand(CString& sLine) { PutStatus(t_p("Disabled {1} channel", "Disabled {1} channels", uDisabled)(uDisabled)); } + } else if (sCommand.Equals("MOVECHAN")) { + if (!m_pNetwork) { + PutStatus(t_s( + "You must be connected with a network to use this command")); + return; + } + + const auto sChan = sLine.Token(1); + const auto sTarget = sLine.Token(2); + if (sChan.empty() || sTarget.empty()) { + PutStatus(t_s("Usage: MoveChan <#chan> ")); + return; + } + + unsigned int uIndex = sTarget.ToUInt(); + + CString sError; + if (m_pNetwork->MoveChan(sChan, uIndex - 1, sError)) + PutStatus(t_f("Moved channel {1} to index {2}")(sChan, uIndex)); + else + PutStatus(sError); + } else if (sCommand.Equals("SWAPCHANS")) { + if (!m_pNetwork) { + PutStatus(t_s( + "You must be connected with a network to use this command")); + return; + } + + const auto sChan1 = sLine.Token(1); + const auto sChan2 = sLine.Token(2); + if (sChan1.empty() || sChan2.empty()) { + PutStatus(t_s("Usage: SwapChans <#chan1> <#chan2>")); + return; + } + + CString sError; + if (m_pNetwork->SwapChans(sChan1, sChan2, sError)) + PutStatus(t_f("Swapped channels {1} and {2}")(sChan1, sChan2)); + else + PutStatus(sError); } else if (sCommand.Equals("LISTCHANS")) { if (!m_pNetwork) { PutStatus(t_s( @@ -496,6 +536,7 @@ void CClient::UserCommand(CString& sLine) { } CTable Table; + Table.AddColumn(t_s("Index", "listchans")); Table.AddColumn(t_s("Name", "listchans")); Table.AddColumn(t_s("Status", "listchans")); Table.AddColumn(t_s("In config", "listchans")); @@ -508,10 +549,12 @@ void CClient::UserCommand(CString& sLine) { Table.AddColumn(CString(cPerm)); } - unsigned int uNumDetached = 0, uNumDisabled = 0, uNumJoined = 0; + unsigned int uNumDetached = 0, uNumDisabled = 0, uNumJoined = 0, + uChanIndex = 1; for (const CChan* pChan : vChans) { Table.AddRow(); + Table.SetCell(t_s("Index", "listchans"), CString(uChanIndex)); Table.SetCell(t_s("Name", "listchans"), pChan->GetPermStr() + pChan->GetName()); Table.SetCell( @@ -545,6 +588,8 @@ void CClient::UserCommand(CString& sLine) { if (pChan->IsDetached()) uNumDetached++; if (pChan->IsOn()) uNumJoined++; if (pChan->IsDisabled()) uNumDisabled++; + + uChanIndex++; } PutStatus(Table); @@ -1781,6 +1826,11 @@ void CClient::HelpUser(const CString& sFilter) { t_s("Enable channels", "helpcmd|EnableChan|desc")); AddCommandHelp("DisableChan", t_s("<#chans>", "helpcmd|DisableChan|args"), t_s("Disable channels", "helpcmd|DisableChan|desc")); + AddCommandHelp("MoveChan", t_s("<#chan> ", "helpcmd|MoveChan|args"), + t_s("Move channel in sort order", "helpcmd|MoveChan|desc")); + AddCommandHelp( + "SwapChans", t_s("<#chan1> <#chan2>", "helpcmd|SwapChans|args"), + t_s("Swap channels in sort order", "helpcmd|SwapChans|desc")); AddCommandHelp("Attach", t_s("<#chans>", "helpcmd|Attach|args"), t_s("Attach to channels", "helpcmd|Attach|desc")); AddCommandHelp("Detach", t_s("<#chans>", "helpcmd|Detach|args"), diff --git a/src/Config.cpp b/src/Config.cpp index 520e4139..a3df463a 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -68,6 +68,7 @@ bool CConfig::Parse(CFile& file, CString& sErrorMsg) { std::stringstream stream; \ stream << "Error on line " << uLineNum << ": " << arg; \ sErrorMsg = stream.str(); \ + m_SubConfigNameSets.clear(); \ m_SubConfigs.clear(); \ m_ConfigEntries.clear(); \ return false; \ @@ -122,14 +123,16 @@ bool CConfig::Parse(CFile& file, CString& sErrorMsg) { else pActiveConfig = &ConfigStack.top().Config; - SubConfig& conf = pActiveConfig->m_SubConfigs[sTag.AsLower()]; - SubConfig::const_iterator it = conf.find(sName); + const auto sTagLower = sTag.AsLower(); + auto& nameset = pActiveConfig->m_SubConfigNameSets[sTagLower]; - if (it != conf.end()) + if (nameset.find(sName) != nameset.end()) ERROR("Duplicate entry for tag \"" << sTag << "\" name \"" << sName << "\"."); - conf[sName] = CConfigEntry(myConfig); + nameset.insert(sName); + pActiveConfig->m_SubConfigs[sTagLower].emplace_back(sName, + myConfig); } else { if (sValue.empty()) ERROR("Empty block name at begin of block."); diff --git a/src/IRCNetwork.cpp b/src/IRCNetwork.cpp index 2855f526..cbe0d538 100644 --- a/src/IRCNetwork.cpp +++ b/src/IRCNetwork.cpp @@ -942,6 +942,49 @@ bool CIRCNetwork::DelChan(const CString& sName) { return false; } +bool CIRCNetwork::MoveChan(const CString& sChan, unsigned int uIndex, + CString& sError) { + if (uIndex >= m_vChans.size()) { + sError = t_s("Invalid index"); + return false; + } + + auto it = m_vChans.begin(); + for (; it != m_vChans.end(); ++it) + if ((*it)->GetName().Equals(sChan)) break; + if (it == m_vChans.end()) { + sError = t_f("You are not on {1}")(sChan); + return false; + } + + const auto pChan = *it; + m_vChans.erase(it); + m_vChans.insert(m_vChans.begin() + uIndex, pChan); + return true; +} + +bool CIRCNetwork::SwapChans(const CString& sChan1, const CString& sChan2, + CString& sError) { + auto it1 = m_vChans.begin(); + for (; it1 != m_vChans.end(); ++it1) + if ((*it1)->GetName().Equals(sChan1)) break; + if (it1 == m_vChans.end()) { + sError = t_f("You are not on {1}")(sChan1); + return false; + } + + auto it2 = m_vChans.begin(); + for (; it2 != m_vChans.end(); ++it2) + if ((*it2)->GetName().Equals(sChan2)) break; + if (it2 == m_vChans.end()) { + sError = t_f("You are not on {1}")(sChan2); + return false; + } + + std::swap(*it1, *it2); + return true; +} + void CIRCNetwork::JoinChans() { // Avoid divsion by zero, it's bad! if (m_vChans.empty()) return; diff --git a/test/ConfigTest.cpp b/test/ConfigTest.cpp index 3795ea10..30533536 100644 --- a/test/ConfigTest.cpp +++ b/test/ConfigTest.cpp @@ -87,8 +87,7 @@ class CConfigSuccessTest : public CConfigTest { CConfig::SubConfigMapIterator it2 = conf.BeginSubConfigs(); while (it2 != conf.EndSubConfigs()) { - std::map::const_iterator it3 = - it2->second.begin(); + auto it3 = it2->second.begin(); while (it3 != it2->second.end()) { sRes += "->" + it2->first + "/" + it3->first + "\n"; @@ -146,6 +145,11 @@ TEST_F(CConfigSuccessTest, SubConf8) { TEST_SUCCESS(" \t \nfoo = bar\n\tFooO = bar\n", "->a/B\nfoo=bar\nfooo=bar\n<-\n"); } +// ensure order is preserved i.e. subconfigs should not be sorted by name +TEST_F(CConfigSuccessTest, SubConf9) { + TEST_SUCCESS("\n\n\n", + "->foo/b\n<-\n->foo/a\n<-\n"); +} /* comments */ TEST_F(CConfigSuccessTest, Comment1) { diff --git a/test/integration/tests/core.cpp b/test/integration/tests/core.cpp index f50a32c7..c5531d05 100644 --- a/test/integration/tests/core.cpp +++ b/test/integration/tests/core.cpp @@ -307,5 +307,41 @@ TEST_F(ZNCTest, StatusEchoMessage) { client3.ReadUntil(":*status!znc@znc.in PRIVMSG nick :Unknown command"); } +TEST_F(ZNCTest, MoveChannels) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + + auto client = LoginClient(); + client.Write("JOIN #foo,#bar"); + client.Close(); + + ircd.Write(":server 001 nick :Hello"); + ircd.ReadUntil("JOIN #foo,#bar"); + ircd.Write(":nick JOIN :#foo"); + ircd.Write(":server 353 nick #foo :nick"); + ircd.Write(":server 366 nick #foo :End of /NAMES list"); + ircd.Write(":nick JOIN :#bar"); + ircd.Write(":server 353 nick #bar :nick"); + ircd.Write(":server 366 nick #bar :End of /NAMES list"); + + client = LoginClient(); + client.ReadUntil(":nick JOIN :#foo"); + client.ReadUntil(":nick JOIN :#bar"); + client.Write("znc movechan #foo 2"); + client.ReadUntil("Moved channel #foo to index 2"); + client.Close(); + + client = LoginClient(); + client.ReadUntil(":nick JOIN :#bar"); + client.ReadUntil(":nick JOIN :#foo"); + client.Write("znc swapchans #foo #bar"); + client.ReadUntil("Swapped channels #foo and #bar"); + client.Close(); + + client = LoginClient(); + client.ReadUntil(":nick JOIN :#foo"); + client.ReadUntil(":nick JOIN :#bar"); +} + } // namespace } // namespace znc_inttest diff --git a/webskins/_default_/pub/_default_.css b/webskins/_default_/pub/_default_.css index e1cae329..78528a72 100644 --- a/webskins/_default_/pub/_default_.css +++ b/webskins/_default_/pub/_default_.css @@ -393,3 +393,8 @@ td { .textsection p { margin-bottom: 0.7em; } + +input.channel_index { + width: 3em; + min-width: unset; +}