Merge branch 'reorder'

pull/1768/head
Alexey Sokolov 2 years ago
commit 1580140b3b

@ -35,10 +35,11 @@ struct CConfigEntry {
class CConfig {
public:
CConfig() : m_ConfigEntries(), m_SubConfigs() {}
CConfig() : m_ConfigEntries(), m_SubConfigs(), m_SubConfigNameSets() {}
typedef std::map<CString, VCString> EntryMap;
typedef std::map<CString, CConfigEntry> SubConfig;
typedef std::pair<CString, CConfigEntry> SubConfigEntry;
typedef std::vector<SubConfigEntry> SubConfig;
typedef std::map<CString, SubConfig> 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<CString, SubConfigNameSet> SubConfigNameSetMap;
EntryMap m_ConfigEntries;
SubConfigMap m_SubConfigs;
SubConfigNameSetMap m_SubConfigNameSets;
};
#endif // !ZNC_CONFIG_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<CChan*>& sChans);

@ -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 = "";

@ -184,6 +184,7 @@
<tr>
<td>[<a href="<? VAR URIPrefix TOP ?><? VAR ModPath TOP ?>addchan?user=<? VAR Username ESC=URL ?>&amp;network=<? VAR Name ESC=URL ?>"><? FORMAT "Add" ?></a>]</td>
<? IF ChannelLoop ?>
<th><? FORMAT "Index" ?></th>
<th><? FORMAT "Save" ?></th>
<th><? FORMAT "Name" ?></th>
<th><? FORMAT "CurModes" ?></th>
@ -196,13 +197,14 @@
</tr>
</thead>
<tbody>
<? LOOP ChannelLoop SORTASC=Name ?>
<tbody id="channels">
<? LOOP ChannelLoop ?>
<tr class="<? IF __EVEN__ ?>evenrow<? ELSE ?>oddrow<? ENDIF ?>">
<td>
<input type="hidden" name="channel" value="<? VAR Name ?>" />
[<a href="<? VAR URIPrefix TOP ?><? VAR ModPath TOP ?>editchan?user=<? VAR Username ESC=URL ?>&amp;network=<? VAR Network ESC=URL ?>&amp;name=<? VAR Name ESC=URL ?>"><? FORMAT "Edit" ?></a>] [<a href="<? VAR URIPrefix TOP ?><? VAR ModPath TOP ?>delchan?user=<? VAR Username ESC=URL ?>&amp;network=<? VAR Network ESC=URL ?>&amp;name=<? VAR Name ESC=URL ?>"><? FORMAT "Del" ?></a>]
</td>
<td><input class="channel_index" type="number" name="index_<? VAR Name ?>" min="1" max="<? VAR MaxIndex ?>" value="<? VAR Index ?>"/></td>
<td><input type="checkbox" name="save_<? VAR Name ?>"<? IF InConfig ?> checked="checked"<? ENDIF ?> /></td>
<td><? VAR Name ?></td>
<td><? VAR CurModes ?></td>
@ -213,6 +215,7 @@
<? ENDLOOP ?>
</tbody>
</table>
<script>channellist_init(jQuery);</script>
</div>
</div>
<? ENDIF ?>

@ -1001,6 +1001,7 @@ class CWebAdminMod : public CModule {
}
const vector<CChan*>& 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());
}
}

@ -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> <index>"));
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> <index>", "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"),

@ -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.");

@ -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;

@ -87,8 +87,7 @@ class CConfigSuccessTest : public CConfigTest {
CConfig::SubConfigMapIterator it2 = conf.BeginSubConfigs();
while (it2 != conf.EndSubConfigs()) {
std::map<CString, CConfigEntry>::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 <A B>\nfoo = bar\n\tFooO = bar\n</a>",
"->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("<foo b>\n</foo>\n<foo a>\n</foo>",
"->foo/b\n<-\n->foo/a\n<-\n");
}
/* comments */
TEST_F(CConfigSuccessTest, Comment1) {

@ -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

@ -393,3 +393,8 @@ td {
.textsection p {
margin-bottom: 0.7em;
}
input.channel_index {
width: 3em;
min-width: unset;
}

Loading…
Cancel
Save