Improve database and RPC API for moving and linking channels, improve test legibility
This commit is contained in:
parent
439f627d9a
commit
cda54b8b5f
8 changed files with 521 additions and 531 deletions
|
@ -133,7 +133,7 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn index_of_channel(&self, channel_id: ChannelId) -> Option<usize> {
|
pub fn index_of_channel(&self, channel_id: ChannelId) -> Option<usize> {
|
||||||
self.channel_paths
|
self.channel_index
|
||||||
.iter()
|
.iter()
|
||||||
.position(|path| path.ends_with(&[channel_id]))
|
.position(|path| path.ends_with(&[channel_id]))
|
||||||
}
|
}
|
||||||
|
@ -327,11 +327,43 @@ impl ChannelStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn link_channel(
|
||||||
|
&mut self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
to: ChannelId,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.spawn(|_, _| async move {
|
||||||
|
let _ = client
|
||||||
|
.request(proto::LinkChannel { channel_id, to })
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlink_channel(
|
||||||
|
&mut self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
from: Option<ChannelId>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.spawn(|_, _| async move {
|
||||||
|
let _ = client
|
||||||
|
.request(proto::UnlinkChannel { channel_id, from })
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_channel(
|
pub fn move_channel(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
from_parent: Option<ChannelId>,
|
from: Option<ChannelId>,
|
||||||
to: Option<ChannelId>,
|
to: ChannelId,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
|
@ -339,7 +371,7 @@ impl ChannelStore {
|
||||||
let _ = client
|
let _ = client
|
||||||
.request(proto::MoveChannel {
|
.request(proto::MoveChannel {
|
||||||
channel_id,
|
channel_id,
|
||||||
from_parent,
|
from,
|
||||||
to,
|
to,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -802,6 +834,4 @@ impl ChannelStore {
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -761,20 +761,41 @@ impl Database {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn link_channel(
|
// Insert an edge from the given channel to the given other channel.
|
||||||
|
pub async fn link_channel(
|
||||||
&self,
|
&self,
|
||||||
from: ChannelId,
|
user: UserId,
|
||||||
|
channel: ChannelId,
|
||||||
|
to: ChannelId,
|
||||||
|
) -> Result<Vec<Channel>> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
// Note that even with these maxed permissions, this linking operation
|
||||||
|
// is still insecure because you can't remove someone's permissions to a
|
||||||
|
// channel if they've linked the channel to one where they're an admin.
|
||||||
|
self.check_user_is_channel_admin(channel, user, &*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.link_channel_internal(user, channel, to, &*tx).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn link_channel_internal(
|
||||||
|
&self,
|
||||||
|
user: UserId,
|
||||||
|
channel: ChannelId,
|
||||||
to: ChannelId,
|
to: ChannelId,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<ChannelDescendants> {
|
) -> Result<Vec<Channel>> {
|
||||||
|
self.check_user_is_channel_admin(to, user, &*tx).await?;
|
||||||
|
|
||||||
let to_ancestors = self.get_channel_ancestors(to, &*tx).await?;
|
let to_ancestors = self.get_channel_ancestors(to, &*tx).await?;
|
||||||
let from_descendants = self.get_channel_descendants([from], &*tx).await?;
|
let mut from_descendants = self.get_channel_descendants([channel], &*tx).await?;
|
||||||
for ancestor in to_ancestors {
|
for ancestor in to_ancestors {
|
||||||
if from_descendants.contains_key(&ancestor) {
|
if from_descendants.contains_key(&ancestor) {
|
||||||
return Err(anyhow!("Cannot create a channel cycle").into());
|
return Err(anyhow!("Cannot create a channel cycle").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let sql = r#"
|
let sql = r#"
|
||||||
INSERT INTO channel_paths
|
INSERT INTO channel_paths
|
||||||
(id_path, channel_id)
|
(id_path, channel_id)
|
||||||
|
@ -790,14 +811,13 @@ impl Database {
|
||||||
self.pool.get_database_backend(),
|
self.pool.get_database_backend(),
|
||||||
sql,
|
sql,
|
||||||
[
|
[
|
||||||
from.to_proto().into(),
|
channel.to_proto().into(),
|
||||||
from.to_proto().into(),
|
channel.to_proto().into(),
|
||||||
to.to_proto().into(),
|
to.to_proto().into(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
tx.execute(channel_paths_stmt).await?;
|
tx.execute(channel_paths_stmt).await?;
|
||||||
|
for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id != &&channel) {
|
||||||
for (from_id, to_ids) in from_descendants.iter().filter(|(id, _)| id != &&from) {
|
|
||||||
for to_id in to_ids {
|
for to_id in to_ids {
|
||||||
let channel_paths_stmt = Statement::from_sql_and_values(
|
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||||
self.pool.get_database_backend(),
|
self.pool.get_database_backend(),
|
||||||
|
@ -812,94 +832,116 @@ impl Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(from_descendants)
|
if let Some(channel) = from_descendants.get_mut(&channel) {
|
||||||
|
// Remove the other parents
|
||||||
|
channel.clear();
|
||||||
|
channel.insert(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
let channels = self.get_all_channels(from_descendants, &*tx).await?;
|
||||||
|
|
||||||
|
Ok(channels)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_channel_from_parent(
|
/// Unlink a channel from a given parent. This will add in a root edge if
|
||||||
|
/// the channel has no other parents after this operation.
|
||||||
|
pub async fn unlink_channel(
|
||||||
&self,
|
&self,
|
||||||
from: ChannelId,
|
user: UserId,
|
||||||
parent: ChannelId,
|
channel: ChannelId,
|
||||||
|
from: Option<ChannelId>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
// Note that even with these maxed permissions, this linking operation
|
||||||
|
// is still insecure because you can't remove someone's permissions to a
|
||||||
|
// channel if they've linked the channel to one where they're an admin.
|
||||||
|
self.check_user_is_channel_admin(channel, user, &*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.unlink_channel_internal(user, channel, from, &*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unlink_channel_internal(
|
||||||
|
&self,
|
||||||
|
user: UserId,
|
||||||
|
channel: ChannelId,
|
||||||
|
from: Option<ChannelId>,
|
||||||
tx: &DatabaseTransaction,
|
tx: &DatabaseTransaction,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let sql = r#"
|
if let Some(from) = from {
|
||||||
|
self.check_user_is_channel_admin(from, user, &*tx).await?;
|
||||||
|
|
||||||
|
let sql = r#"
|
||||||
DELETE FROM channel_paths
|
DELETE FROM channel_paths
|
||||||
WHERE
|
WHERE
|
||||||
id_path LIKE '%' || $1 || '/' || $2 || '%'
|
id_path LIKE '%' || $1 || '/' || $2 || '%'
|
||||||
"#;
|
"#;
|
||||||
|
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||||
|
self.pool.get_database_backend(),
|
||||||
|
sql,
|
||||||
|
[from.to_proto().into(), channel.to_proto().into()],
|
||||||
|
);
|
||||||
|
tx.execute(channel_paths_stmt).await?;
|
||||||
|
} else {
|
||||||
|
let sql = r#"
|
||||||
|
DELETE FROM channel_paths
|
||||||
|
WHERE
|
||||||
|
id_path = '/' || $1 || '/'
|
||||||
|
"#;
|
||||||
|
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||||
|
self.pool.get_database_backend(),
|
||||||
|
sql,
|
||||||
|
[channel.to_proto().into()],
|
||||||
|
);
|
||||||
|
tx.execute(channel_paths_stmt).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that there is always at least one path to the channel
|
||||||
|
let sql = r#"
|
||||||
|
INSERT INTO channel_paths
|
||||||
|
(id_path, channel_id)
|
||||||
|
SELECT
|
||||||
|
'/' || $1 || '/', $2
|
||||||
|
WHERE NOT EXISTS
|
||||||
|
(SELECT *
|
||||||
|
FROM channel_paths
|
||||||
|
WHERE channel_id = $2)
|
||||||
|
"#;
|
||||||
|
|
||||||
let channel_paths_stmt = Statement::from_sql_and_values(
|
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||||
self.pool.get_database_backend(),
|
self.pool.get_database_backend(),
|
||||||
sql,
|
sql,
|
||||||
[parent.to_proto().into(), from.to_proto().into()],
|
[channel.to_proto().into(), channel.to_proto().into()],
|
||||||
);
|
);
|
||||||
tx.execute(channel_paths_stmt).await?;
|
tx.execute(channel_paths_stmt).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move a channel from one parent to another.
|
/// Move a channel from one parent to another, returns the
|
||||||
/// Note that this requires a valid parent_id in the 'from_parent' field.
|
/// Channels that were moved for notifying clients
|
||||||
/// As channels are a DAG, we need to know which parent to remove the channel from.
|
|
||||||
/// Here's a list of the parameters to this function and their behavior:
|
|
||||||
///
|
|
||||||
/// - (`None`, `None`) No op
|
|
||||||
/// - (`None`, `Some(id)`) Link the channel without removing it from any of it's parents
|
|
||||||
/// - (`Some(id)`, `None`) Remove a channel from a given parent, and leave other parents
|
|
||||||
/// - (`Some(id)`, `Some(id)`) Move channel from one parent to another, leaving other parents
|
|
||||||
///
|
|
||||||
/// Returns the channel that was moved + it's sub channels for use
|
|
||||||
/// by the members for `to`
|
|
||||||
pub async fn move_channel(
|
pub async fn move_channel(
|
||||||
&self,
|
&self,
|
||||||
user: UserId,
|
user: UserId,
|
||||||
from: ChannelId,
|
channel: ChannelId,
|
||||||
from_parent: Option<ChannelId>,
|
from: Option<ChannelId>,
|
||||||
to: Option<ChannelId>,
|
to: ChannelId,
|
||||||
) -> Result<Vec<Channel>> {
|
) -> Result<Vec<Channel>> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
// Note that even with these maxed permissions, this linking operation
|
self.check_user_is_channel_admin(channel, user, &*tx)
|
||||||
// is still insecure because you can't remove someone's permissions to a
|
.await?;
|
||||||
// channel if they've linked the channel to one where they're an admin.
|
|
||||||
self.check_user_is_channel_admin(from, user, &*tx).await?;
|
|
||||||
|
|
||||||
let mut channel_descendants = None;
|
let moved_channels = self.link_channel_internal(user, channel, to, &*tx).await?;
|
||||||
|
|
||||||
// Note that we have to do the linking before the removal, so that we
|
self.unlink_channel_internal(user, channel, from, &*tx)
|
||||||
// can leave the channel_path table in a consistent state.
|
.await?;
|
||||||
if let Some(to) = to {
|
|
||||||
self.check_user_is_channel_admin(to, user, &*tx).await?;
|
|
||||||
|
|
||||||
channel_descendants = Some(self.link_channel(from, to, &*tx).await?);
|
Ok(moved_channels)
|
||||||
}
|
|
||||||
|
|
||||||
let mut channel_descendants = match channel_descendants {
|
|
||||||
Some(channel_descendants) => channel_descendants,
|
|
||||||
None => self.get_channel_descendants([from], &*tx).await?,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(from_parent) = from_parent {
|
|
||||||
self.check_user_is_channel_admin(from_parent, user, &*tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.remove_channel_from_parent(from, from_parent, &*tx)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let channels;
|
|
||||||
if let Some(to) = to {
|
|
||||||
if let Some(channel) = channel_descendants.get_mut(&from) {
|
|
||||||
// Remove the other parents
|
|
||||||
channel.clear();
|
|
||||||
channel.insert(to);
|
|
||||||
}
|
|
||||||
|
|
||||||
channels = self
|
|
||||||
.get_all_channels(channel_descendants, &*tx)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
channels = vec![];
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(channels)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use rpc::{proto, ConnectionId};
|
use rpc::{proto, ConnectionId};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{Channel, Database, NewUserParams},
|
db::{Channel, ChannelId, Database, NewUserParams},
|
||||||
test_both_dbs,
|
test_both_dbs,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -501,50 +501,32 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
// sanity check
|
// sanity check
|
||||||
// Initial DAG:
|
// Initial DAG:
|
||||||
// /- gpui2
|
// /- gpui2
|
||||||
// zed -- crdb - livestreaming - livestreaming_dag
|
// zed -- crdb - livestreaming - livestreaming_dag
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
pretty_assertions::assert_eq!(
|
assert_dag(
|
||||||
result.channels,
|
result.channels,
|
||||||
vec![
|
&[
|
||||||
Channel {
|
(zed_id, None),
|
||||||
id: zed_id,
|
(crdb_id, Some(zed_id)),
|
||||||
name: "zed".to_string(),
|
(gpui2_id, Some(zed_id)),
|
||||||
parent_id: None,
|
(livestreaming_id, Some(crdb_id)),
|
||||||
},
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
Channel {
|
],
|
||||||
id: crdb_id,
|
|
||||||
name: "crdb".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: gpui2_id,
|
|
||||||
name: "gpui2".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(crdb_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_id,
|
|
||||||
name: "livestreaming_dag".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Attempt to make a cycle
|
// Attempt to make a cycle
|
||||||
assert!(db
|
assert!(db
|
||||||
.move_channel(a_id, zed_id, None, Some(livestreaming_id))
|
.link_channel(a_id, zed_id, livestreaming_id)
|
||||||
.await
|
.await
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
// Make a link
|
// Make a link
|
||||||
db.move_channel(a_id, livestreaming_id, None, Some(zed_id))
|
db.link_channel(a_id, livestreaming_id, zed_id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -553,42 +535,16 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
// zed -- crdb - livestreaming - livestreaming_dag
|
// zed -- crdb - livestreaming - livestreaming_dag
|
||||||
// \---------/
|
// \---------/
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
pretty_assertions::assert_eq!(
|
assert_dag(result.channels, &[
|
||||||
result.channels,
|
(zed_id, None),
|
||||||
vec![
|
(crdb_id, Some(zed_id)),
|
||||||
Channel {
|
(gpui2_id, Some(zed_id)),
|
||||||
id: zed_id,
|
(livestreaming_id, Some(zed_id)),
|
||||||
name: "zed".to_string(),
|
(livestreaming_id, Some(crdb_id)),
|
||||||
parent_id: None,
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
},
|
]);
|
||||||
Channel {
|
|
||||||
id: crdb_id,
|
|
||||||
name: "crdb".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: gpui2_id,
|
|
||||||
name: "gpui2".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(crdb_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_id,
|
|
||||||
name: "livestreaming_dag".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
// Create a new channel below a channel with multiple parents
|
// Create a new channel below a channel with multiple parents
|
||||||
let livestreaming_dag_sub_id = db
|
let livestreaming_dag_sub_id = db
|
||||||
.create_channel(
|
.create_channel(
|
||||||
|
@ -605,50 +561,20 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
// zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id
|
// zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id
|
||||||
// \---------/
|
// \---------/
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
pretty_assertions::assert_eq!(
|
assert_dag(result.channels, &[
|
||||||
result.channels,
|
(zed_id, None),
|
||||||
vec![
|
(crdb_id, Some(zed_id)),
|
||||||
Channel {
|
(gpui2_id, Some(zed_id)),
|
||||||
id: zed_id,
|
(livestreaming_id, Some(zed_id)),
|
||||||
name: "zed".to_string(),
|
(livestreaming_id, Some(crdb_id)),
|
||||||
parent_id: None,
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
},
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
Channel {
|
]);
|
||||||
id: crdb_id,
|
|
||||||
name: "crdb".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: gpui2_id,
|
|
||||||
name: "gpui2".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(crdb_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_id,
|
|
||||||
name: "livestreaming_dag".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_sub_id,
|
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
|
||||||
parent_id: Some(livestreaming_dag_id),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make a link
|
// ========================================================================
|
||||||
let channels = db
|
// Test a complex DAG by making another link
|
||||||
.move_channel(a_id, livestreaming_dag_sub_id, None, Some(livestreaming_id))
|
let returned_channels = db
|
||||||
|
.link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -658,66 +584,32 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
// \--------/
|
// \--------/
|
||||||
|
|
||||||
// make sure we're getting just the new link
|
// make sure we're getting just the new link
|
||||||
|
// Not using the assert_dag helper because we want to make sure we're returning the full data
|
||||||
pretty_assertions::assert_eq!(
|
pretty_assertions::assert_eq!(
|
||||||
channels,
|
returned_channels,
|
||||||
vec![
|
vec![Channel {
|
||||||
Channel {
|
id: livestreaming_dag_sub_id,
|
||||||
id: livestreaming_dag_sub_id,
|
name: "livestreaming_dag_sub".to_string(),
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
parent_id: Some(livestreaming_id),
|
||||||
parent_id: Some(livestreaming_id),
|
}]
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
pretty_assertions::assert_eq!(
|
assert_dag(result.channels, &[
|
||||||
result.channels,
|
(zed_id, None),
|
||||||
vec![
|
(crdb_id, Some(zed_id)),
|
||||||
Channel {
|
(gpui2_id, Some(zed_id)),
|
||||||
id: zed_id,
|
(livestreaming_id, Some(zed_id)),
|
||||||
name: "zed".to_string(),
|
(livestreaming_id, Some(crdb_id)),
|
||||||
parent_id: None,
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
},
|
(livestreaming_dag_sub_id, Some(livestreaming_id)),
|
||||||
Channel {
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
id: crdb_id,
|
]);
|
||||||
name: "crdb".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: gpui2_id,
|
|
||||||
name: "gpui2".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(crdb_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_id,
|
|
||||||
name: "livestreaming_dag".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_sub_id,
|
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_sub_id,
|
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
|
||||||
parent_id: Some(livestreaming_dag_id),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make another link
|
// ========================================================================
|
||||||
let channels = db.move_channel(a_id, livestreaming_id, None, Some(gpui2_id))
|
// Test a complex DAG by making another link
|
||||||
|
let returned_channels = db
|
||||||
|
.link_channel(a_id, livestreaming_id, gpui2_id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -727,62 +619,14 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
// \---------/
|
// \---------/
|
||||||
|
|
||||||
// Make sure that we're correctly getting the full sub-dag
|
// Make sure that we're correctly getting the full sub-dag
|
||||||
pretty_assertions::assert_eq!(channels,
|
|
||||||
vec![Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(gpui2_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_id,
|
|
||||||
name: "livestreaming_dag".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_sub_id,
|
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_sub_id,
|
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
|
||||||
parent_id: Some(livestreaming_dag_id),
|
|
||||||
}]);
|
|
||||||
|
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
|
||||||
pretty_assertions::assert_eq!(
|
pretty_assertions::assert_eq!(
|
||||||
result.channels,
|
returned_channels,
|
||||||
vec![
|
vec![
|
||||||
Channel {
|
|
||||||
id: zed_id,
|
|
||||||
name: "zed".to_string(),
|
|
||||||
parent_id: None,
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: crdb_id,
|
|
||||||
name: "crdb".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: gpui2_id,
|
|
||||||
name: "gpui2".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
Channel {
|
||||||
id: livestreaming_id,
|
id: livestreaming_id,
|
||||||
name: "livestreaming".to_string(),
|
name: "livestreaming".to_string(),
|
||||||
parent_id: Some(gpui2_id),
|
parent_id: Some(gpui2_id),
|
||||||
},
|
},
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(crdb_id),
|
|
||||||
},
|
|
||||||
Channel {
|
Channel {
|
||||||
id: livestreaming_dag_id,
|
id: livestreaming_dag_id,
|
||||||
name: "livestreaming_dag".to_string(),
|
name: "livestreaming_dag".to_string(),
|
||||||
|
@ -797,12 +641,31 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
id: livestreaming_dag_sub_id,
|
id: livestreaming_dag_sub_id,
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
name: "livestreaming_dag_sub".to_string(),
|
||||||
parent_id: Some(livestreaming_dag_id),
|
parent_id: Some(livestreaming_dag_id),
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove that inner link
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
let channels = db.move_channel(a_id, livestreaming_dag_sub_id, Some(livestreaming_id), None)
|
assert_dag(result.channels, &[
|
||||||
|
(zed_id, None),
|
||||||
|
(crdb_id, Some(zed_id)),
|
||||||
|
(gpui2_id, Some(zed_id)),
|
||||||
|
(livestreaming_id, Some(gpui2_id)),
|
||||||
|
(livestreaming_id, Some(zed_id)),
|
||||||
|
(livestreaming_id, Some(crdb_id)),
|
||||||
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
|
(livestreaming_dag_sub_id, Some(livestreaming_id)),
|
||||||
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Test unlinking in a complex DAG by removing the inner link
|
||||||
|
db
|
||||||
|
.unlink_channel(
|
||||||
|
a_id,
|
||||||
|
livestreaming_dag_sub_id,
|
||||||
|
Some(livestreaming_id),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -811,62 +674,21 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
// zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
|
// zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
|
||||||
// \---------/
|
// \---------/
|
||||||
|
|
||||||
// Since we're not moving it to anywhere, there's nothing to notify anyone about
|
|
||||||
pretty_assertions::assert_eq!(
|
|
||||||
channels,
|
|
||||||
vec![]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
pretty_assertions::assert_eq!(
|
assert_dag(result.channels, &[
|
||||||
result.channels,
|
(zed_id, None),
|
||||||
vec![
|
(crdb_id, Some(zed_id)),
|
||||||
Channel {
|
(gpui2_id, Some(zed_id)),
|
||||||
id: zed_id,
|
(livestreaming_id, Some(gpui2_id)),
|
||||||
name: "zed".to_string(),
|
(livestreaming_id, Some(zed_id)),
|
||||||
parent_id: None,
|
(livestreaming_id, Some(crdb_id)),
|
||||||
},
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
Channel {
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
id: crdb_id,
|
]);
|
||||||
name: "crdb".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: gpui2_id,
|
|
||||||
name: "gpui2".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(gpui2_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(crdb_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_id,
|
|
||||||
name: "livestreaming_dag".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_sub_id,
|
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
|
||||||
parent_id: Some(livestreaming_dag_id),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove that outer link
|
// ========================================================================
|
||||||
db.move_channel(a_id, livestreaming_id, Some(gpui2_id), None)
|
// Test unlinking in a complex DAG by removing the inner link
|
||||||
|
db.unlink_channel(a_id, livestreaming_id, Some(gpui2_id))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -875,49 +697,19 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
// zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
|
// zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
|
||||||
// \---------/
|
// \---------/
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
pretty_assertions::assert_eq!(
|
assert_dag(result.channels, &[
|
||||||
result.channels,
|
(zed_id, None),
|
||||||
vec![
|
(crdb_id, Some(zed_id)),
|
||||||
Channel {
|
(gpui2_id, Some(zed_id)),
|
||||||
id: zed_id,
|
(livestreaming_id, Some(zed_id)),
|
||||||
name: "zed".to_string(),
|
(livestreaming_id, Some(crdb_id)),
|
||||||
parent_id: None,
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
},
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
Channel {
|
]);
|
||||||
id: crdb_id,
|
|
||||||
name: "crdb".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: gpui2_id,
|
|
||||||
name: "gpui2".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(crdb_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_id,
|
|
||||||
name: "livestreaming_dag".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_sub_id,
|
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
|
||||||
parent_id: Some(livestreaming_dag_id),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Move livestreaming to be below gpui2
|
// ========================================================================
|
||||||
db.move_channel(a_id, livestreaming_id, Some(crdb_id), Some(gpui2_id))
|
// Test moving DAG nodes by moving livestreaming to be below gpui2
|
||||||
|
db.move_channel(a_id, livestreaming_id, Some(crdb_id), gpui2_id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -926,47 +718,17 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
// zed - crdb /
|
// zed - crdb /
|
||||||
// \---------/
|
// \---------/
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
pretty_assertions::assert_eq!(
|
assert_dag(result.channels, &[
|
||||||
result.channels,
|
(zed_id, None),
|
||||||
vec![
|
(crdb_id, Some(zed_id)),
|
||||||
Channel {
|
(gpui2_id, Some(zed_id)),
|
||||||
id: zed_id,
|
(livestreaming_id, Some(zed_id)),
|
||||||
name: "zed".to_string(),
|
(livestreaming_id, Some(gpui2_id)),
|
||||||
parent_id: None,
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
},
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
Channel {
|
]);
|
||||||
id: crdb_id,
|
|
||||||
name: "crdb".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: gpui2_id,
|
|
||||||
name: "gpui2".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(gpui2_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_id,
|
|
||||||
name: "livestreaming_dag".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_sub_id,
|
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
|
||||||
parent_id: Some(livestreaming_dag_id),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
// Deleting a channel should not delete children that still have other parents
|
// Deleting a channel should not delete children that still have other parents
|
||||||
db.delete_channel(gpui2_id, a_id).await.unwrap();
|
db.delete_channel(gpui2_id, a_id).await.unwrap();
|
||||||
|
|
||||||
|
@ -974,46 +736,109 @@ async fn test_channels_moving(db: &Arc<Database>) {
|
||||||
// zed - crdb
|
// zed - crdb
|
||||||
// \- livestreaming - livestreaming_dag - livestreaming_dag_sub
|
// \- livestreaming - livestreaming_dag - livestreaming_dag_sub
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
pretty_assertions::assert_eq!(
|
assert_dag(result.channels, &[
|
||||||
result.channels,
|
(zed_id, None),
|
||||||
vec![
|
(crdb_id, Some(zed_id)),
|
||||||
Channel {
|
(livestreaming_id, Some(zed_id)),
|
||||||
id: zed_id,
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
name: "zed".to_string(),
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
parent_id: None,
|
]);
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: crdb_id,
|
|
||||||
name: "crdb".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_id,
|
|
||||||
name: "livestreaming".to_string(),
|
|
||||||
parent_id: Some(zed_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_id,
|
|
||||||
name: "livestreaming_dag".to_string(),
|
|
||||||
parent_id: Some(livestreaming_id),
|
|
||||||
},
|
|
||||||
Channel {
|
|
||||||
id: livestreaming_dag_sub_id,
|
|
||||||
name: "livestreaming_dag_sub".to_string(),
|
|
||||||
parent_id: Some(livestreaming_dag_id),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// But deleting a parent of a DAG should delete the whole DAG:
|
// ========================================================================
|
||||||
db.move_channel(a_id, livestreaming_id, None, Some(crdb_id))
|
// Unlinking a channel from it's parent should automatically promote it to a root channel
|
||||||
|
db.unlink_channel(a_id, crdb_id, Some(zed_id))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// DAG is now:
|
||||||
|
// crdb
|
||||||
|
// zed
|
||||||
|
// \- livestreaming - livestreaming_dag - livestreaming_dag_sub
|
||||||
|
|
||||||
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
|
assert_dag(result.channels, &[
|
||||||
|
(zed_id, None),
|
||||||
|
(crdb_id, None),
|
||||||
|
(livestreaming_id, Some(zed_id)),
|
||||||
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Unlinking a root channel should not have any effect
|
||||||
|
db.unlink_channel(a_id, crdb_id, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// DAG is now:
|
||||||
|
// crdb
|
||||||
|
// zed
|
||||||
|
// \- livestreaming - livestreaming_dag - livestreaming_dag_sub
|
||||||
|
//
|
||||||
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
|
assert_dag(result.channels, &[
|
||||||
|
(zed_id, None),
|
||||||
|
(crdb_id, None),
|
||||||
|
(livestreaming_id, Some(zed_id)),
|
||||||
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// You should be able to move a root channel into a non-root channel
|
||||||
|
db.move_channel(a_id, crdb_id, None, zed_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// DAG is now:
|
||||||
|
// zed - crdb
|
||||||
|
// \- livestreaming - livestreaming_dag - livestreaming_dag_sub
|
||||||
|
|
||||||
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
|
assert_dag(result.channels, &[
|
||||||
|
(zed_id, None),
|
||||||
|
(crdb_id, Some(zed_id)),
|
||||||
|
(livestreaming_id, Some(zed_id)),
|
||||||
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Moving a non-root channel without a parent id should be the equivalent of a link operation
|
||||||
|
db.move_channel(a_id, livestreaming_id, None, crdb_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// DAG is now:
|
// DAG is now:
|
||||||
// zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub
|
// zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub
|
||||||
// \--------/
|
// \--------/
|
||||||
|
|
||||||
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
|
assert_dag(result.channels, &[
|
||||||
|
(zed_id, None),
|
||||||
|
(crdb_id, Some(zed_id)),
|
||||||
|
(livestreaming_id, Some(zed_id)),
|
||||||
|
(livestreaming_id, Some(crdb_id)),
|
||||||
|
(livestreaming_dag_id, Some(livestreaming_id)),
|
||||||
|
(livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Deleting a parent of a DAG should delete the whole DAG:
|
||||||
db.delete_channel(zed_id, a_id).await.unwrap();
|
db.delete_channel(zed_id, a_id).await.unwrap();
|
||||||
let result = db.get_channels_for_user(a_id).await.unwrap();
|
let result = db.get_channels_for_user(a_id).await.unwrap();
|
||||||
assert!(result.channels.is_empty())
|
assert!(
|
||||||
|
result.channels.is_empty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_dag(actual: Vec<Channel>, expected: &[(ChannelId, Option<ChannelId>)]) {
|
||||||
|
let actual = actual
|
||||||
|
.iter()
|
||||||
|
.map(|channel| (channel.id, channel.parent_id))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
pretty_assertions::assert_eq!(actual, expected)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ mod connection_pool;
|
||||||
use crate::{
|
use crate::{
|
||||||
auth,
|
auth,
|
||||||
db::{
|
db::{
|
||||||
self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User,
|
self, Channel, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User,
|
||||||
UserId,
|
UserId,
|
||||||
},
|
},
|
||||||
executor::Executor,
|
executor::Executor,
|
||||||
|
@ -267,6 +267,8 @@ impl Server {
|
||||||
.add_request_handler(send_channel_message)
|
.add_request_handler(send_channel_message)
|
||||||
.add_request_handler(remove_channel_message)
|
.add_request_handler(remove_channel_message)
|
||||||
.add_request_handler(get_channel_messages)
|
.add_request_handler(get_channel_messages)
|
||||||
|
.add_request_handler(link_channel)
|
||||||
|
.add_request_handler(unlink_channel)
|
||||||
.add_request_handler(move_channel)
|
.add_request_handler(move_channel)
|
||||||
.add_request_handler(follow)
|
.add_request_handler(follow)
|
||||||
.add_message_handler(unfollow)
|
.add_message_handler(unfollow)
|
||||||
|
@ -2391,6 +2393,72 @@ async fn rename_channel(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn link_channel(
|
||||||
|
request: proto::LinkChannel,
|
||||||
|
response: Response<proto::LinkChannel>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = session.db().await;
|
||||||
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
|
let to = ChannelId::from_proto(request.to);
|
||||||
|
let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?;
|
||||||
|
|
||||||
|
let members = db.get_channel_members(to).await?;
|
||||||
|
let connection_pool = session.connection_pool().await;
|
||||||
|
let update = proto::UpdateChannels {
|
||||||
|
channels: channels_to_send
|
||||||
|
.into_iter()
|
||||||
|
.map(|channel| proto::Channel {
|
||||||
|
id: channel.id.to_proto(),
|
||||||
|
name: channel.name,
|
||||||
|
parent_id: channel.parent_id.map(ChannelId::to_proto),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for member_id in members {
|
||||||
|
for connection_id in connection_pool.user_connection_ids(member_id) {
|
||||||
|
session.peer.send(connection_id, update.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.send(Ack {})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unlink_channel(
|
||||||
|
request: proto::UnlinkChannel,
|
||||||
|
response: Response<proto::UnlinkChannel>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = session.db().await;
|
||||||
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
|
let from = request.from.map(ChannelId::from_proto);
|
||||||
|
db.unlink_channel(session.user_id, channel_id, from).await?;
|
||||||
|
|
||||||
|
if let Some(from_parent) = from {
|
||||||
|
let members = db.get_channel_members(from_parent).await?;
|
||||||
|
let update = proto::UpdateChannels {
|
||||||
|
delete_channel_edge: vec![proto::ChannelEdge {
|
||||||
|
channel_id: channel_id.to_proto(),
|
||||||
|
parent_id: from_parent.to_proto(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let connection_pool = session.connection_pool().await;
|
||||||
|
for member_id in members {
|
||||||
|
for connection_id in connection_pool.user_connection_ids(member_id) {
|
||||||
|
session.peer.send(connection_id, update.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.send(Ack {})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn move_channel(
|
async fn move_channel(
|
||||||
request: proto::MoveChannel,
|
request: proto::MoveChannel,
|
||||||
response: Response<proto::MoveChannel>,
|
response: Response<proto::MoveChannel>,
|
||||||
|
@ -2398,18 +2466,12 @@ async fn move_channel(
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let db = session.db().await;
|
let db = session.db().await;
|
||||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
let from_parent = request.from_parent.map(ChannelId::from_proto);
|
let from_parent = request.from.map(ChannelId::from_proto);
|
||||||
let to = request.to.map(ChannelId::from_proto);
|
let to = ChannelId::from_proto(request.to);
|
||||||
let channels_to_send = db
|
let channels_to_send: Vec<Channel> = db
|
||||||
.move_channel(
|
.move_channel(session.user_id, channel_id, from_parent, to)
|
||||||
session.user_id,
|
|
||||||
channel_id,
|
|
||||||
from_parent,
|
|
||||||
to,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
||||||
if let Some(from_parent) = from_parent {
|
if let Some(from_parent) = from_parent {
|
||||||
let members = db.get_channel_members(from_parent).await?;
|
let members = db.get_channel_members(from_parent).await?;
|
||||||
let update = proto::UpdateChannels {
|
let update = proto::UpdateChannels {
|
||||||
|
@ -2425,24 +2487,24 @@ async fn move_channel(
|
||||||
session.peer.send(connection_id, update.clone())?;
|
session.peer.send(connection_id, update.clone())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(to) = to {
|
let members = db.get_channel_members(to).await?;
|
||||||
let members = db.get_channel_members(to).await?;
|
let connection_pool = session.connection_pool().await;
|
||||||
let connection_pool = session.connection_pool().await;
|
let update = proto::UpdateChannels {
|
||||||
let update = proto::UpdateChannels {
|
channels: channels_to_send
|
||||||
channels: channels_to_send.into_iter().map(|channel| proto::Channel {
|
.into_iter()
|
||||||
|
.map(|channel| proto::Channel {
|
||||||
id: channel.id.to_proto(),
|
id: channel.id.to_proto(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
parent_id: channel.parent_id.map(ChannelId::to_proto),
|
parent_id: channel.parent_id.map(ChannelId::to_proto),
|
||||||
}).collect(),
|
})
|
||||||
..Default::default()
|
.collect(),
|
||||||
};
|
..Default::default()
|
||||||
for member_id in members {
|
};
|
||||||
for connection_id in connection_pool.user_connection_ids(member_id) {
|
for member_id in members {
|
||||||
session.peer.send(connection_id, update.clone())?;
|
for connection_id in connection_pool.user_connection_ids(member_id) {
|
||||||
}
|
session.peer.send(connection_id, update.clone())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -933,7 +933,7 @@ async fn test_channel_moving(deterministic: Arc<Deterministic>, cx_a: &mut TestA
|
||||||
client_a
|
client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |channel_store, cx| {
|
.update(cx_a, |channel_store, cx| {
|
||||||
channel_store.move_channel(channel_c_id, Some(channel_b_id), Some(channel_a_id), cx)
|
channel_store.move_channel(channel_c_id, Some(channel_b_id), channel_a_id, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -970,7 +970,7 @@ async fn test_channel_moving(deterministic: Arc<Deterministic>, cx_a: &mut TestA
|
||||||
client_a
|
client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |channel_store, cx| {
|
.update(cx_a, |channel_store, cx| {
|
||||||
channel_store.move_channel(channel_c_id, None, Some(channel_b_id), cx)
|
channel_store.link_channel(channel_c_id, channel_b_id, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -114,7 +114,7 @@ struct PutChannel {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
struct UnlinkChannel {
|
struct UnlinkChannel {
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
parent_id: ChannelId,
|
parent_id: Option<ChannelId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
|
@ -199,13 +199,13 @@ pub fn init(cx: &mut AppContext) {
|
||||||
|
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|panel: &mut CollabPanel, action: &LinkChannel, _: &mut ViewContext<CollabPanel>| {
|
|panel: &mut CollabPanel, action: &LinkChannel, _: &mut ViewContext<CollabPanel>| {
|
||||||
panel.copy = Some(ChannelCopy::Link(action.channel_id));
|
panel.link_or_move = Some(ChannelCopy::Link(action.channel_id));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|panel: &mut CollabPanel, action: &MoveChannel, _: &mut ViewContext<CollabPanel>| {
|
|panel: &mut CollabPanel, action: &MoveChannel, _: &mut ViewContext<CollabPanel>| {
|
||||||
panel.copy = Some(ChannelCopy::Move {
|
panel.link_or_move = Some(ChannelCopy::Move {
|
||||||
channel_id: action.channel_id,
|
channel_id: action.channel_id,
|
||||||
parent_id: action.parent_id,
|
parent_id: action.parent_id,
|
||||||
});
|
});
|
||||||
|
@ -214,20 +214,20 @@ pub fn init(cx: &mut AppContext) {
|
||||||
|
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|panel: &mut CollabPanel, action: &PutChannel, cx: &mut ViewContext<CollabPanel>| {
|
|panel: &mut CollabPanel, action: &PutChannel, cx: &mut ViewContext<CollabPanel>| {
|
||||||
if let Some(copy) = panel.copy.take() {
|
if let Some(copy) = panel.link_or_move.take() {
|
||||||
match copy {
|
match copy {
|
||||||
ChannelCopy::Move {
|
ChannelCopy::Move {
|
||||||
channel_id,
|
channel_id,
|
||||||
parent_id,
|
parent_id,
|
||||||
} => panel.channel_store.update(cx, |channel_store, cx| {
|
} => panel.channel_store.update(cx, |channel_store, cx| {
|
||||||
channel_store
|
channel_store
|
||||||
.move_channel(channel_id, parent_id, Some(action.to), cx)
|
.move_channel(channel_id, parent_id, action.to, cx)
|
||||||
.detach_and_log_err(cx)
|
.detach_and_log_err(cx)
|
||||||
}),
|
}),
|
||||||
ChannelCopy::Link(channel) => {
|
ChannelCopy::Link(channel) => {
|
||||||
panel.channel_store.update(cx, |channel_store, cx| {
|
panel.channel_store.update(cx, |channel_store, cx| {
|
||||||
channel_store
|
channel_store
|
||||||
.move_channel(channel, None, Some(action.to), cx)
|
.link_channel(channel, action.to, cx)
|
||||||
.detach_and_log_err(cx)
|
.detach_and_log_err(cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -240,7 +240,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
|panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext<CollabPanel>| {
|
|panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext<CollabPanel>| {
|
||||||
panel.channel_store.update(cx, |channel_store, cx| {
|
panel.channel_store.update(cx, |channel_store, cx| {
|
||||||
channel_store
|
channel_store
|
||||||
.move_channel(action.channel_id, Some(action.parent_id), None, cx)
|
.unlink_channel(action.channel_id, action.parent_id, cx)
|
||||||
.detach_and_log_err(cx)
|
.detach_and_log_err(cx)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -284,13 +284,20 @@ impl ChannelCopy {
|
||||||
ChannelCopy::Link(channel_id) => *channel_id,
|
ChannelCopy::Link(channel_id) => *channel_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_move(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
ChannelCopy::Move { .. } => true,
|
||||||
|
ChannelCopy::Link(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CollabPanel {
|
pub struct CollabPanel {
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
has_focus: bool,
|
has_focus: bool,
|
||||||
copy: Option<ChannelCopy>,
|
link_or_move: Option<ChannelCopy>,
|
||||||
pending_serialization: Task<Option<()>>,
|
pending_serialization: Task<Option<()>>,
|
||||||
context_menu: ViewHandle<ContextMenu>,
|
context_menu: ViewHandle<ContextMenu>,
|
||||||
filter_editor: ViewHandle<Editor>,
|
filter_editor: ViewHandle<Editor>,
|
||||||
|
@ -553,7 +560,7 @@ impl CollabPanel {
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
width: None,
|
width: None,
|
||||||
has_focus: false,
|
has_focus: false,
|
||||||
copy: None,
|
link_or_move: None,
|
||||||
fs: workspace.app_state().fs.clone(),
|
fs: workspace.app_state().fs.clone(),
|
||||||
pending_serialization: Task::ready(None),
|
pending_serialization: Task::ready(None),
|
||||||
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
|
||||||
|
@ -2062,15 +2069,14 @@ impl CollabPanel {
|
||||||
) {
|
) {
|
||||||
self.context_menu_on_selected = position.is_none();
|
self.context_menu_on_selected = position.is_none();
|
||||||
|
|
||||||
let copy_channel = self
|
let operation_details = self.link_or_move.as_ref().and_then(|link_or_move| {
|
||||||
.copy
|
let channel_name = self
|
||||||
.as_ref()
|
.channel_store
|
||||||
.and_then(|copy| {
|
.read(cx)
|
||||||
self.channel_store
|
.channel_for_id(link_or_move.channel_id())
|
||||||
.read(cx)
|
.map(|channel| channel.name.clone())?;
|
||||||
.channel_for_id(copy.channel_id())
|
Some((channel_name, link_or_move.is_move()))
|
||||||
})
|
});
|
||||||
.map(|channel| channel.name.clone());
|
|
||||||
|
|
||||||
self.context_menu.update(cx, |context_menu, cx| {
|
self.context_menu.update(cx, |context_menu, cx| {
|
||||||
context_menu.set_position_mode(if self.context_menu_on_selected {
|
context_menu.set_position_mode(if self.context_menu_on_selected {
|
||||||
|
@ -2079,13 +2085,29 @@ impl CollabPanel {
|
||||||
OverlayPositionMode::Window
|
OverlayPositionMode::Window
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
if let Some((channel_name, is_move)) = operation_details {
|
||||||
|
items.push(ContextMenuItem::action(
|
||||||
|
format!(
|
||||||
|
"{} '#{}' here",
|
||||||
|
if is_move { "Move" } else { "Link" },
|
||||||
|
channel_name
|
||||||
|
),
|
||||||
|
PutChannel {
|
||||||
|
to: location.channel,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
items.push(ContextMenuItem::Separator)
|
||||||
|
}
|
||||||
|
|
||||||
let expand_action_name = if self.is_channel_collapsed(&location) {
|
let expand_action_name = if self.is_channel_collapsed(&location) {
|
||||||
"Expand Subchannels"
|
"Expand Subchannels"
|
||||||
} else {
|
} else {
|
||||||
"Collapse Subchannels"
|
"Collapse Subchannels"
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut items = vec![
|
items.extend([
|
||||||
ContextMenuItem::action(
|
ContextMenuItem::action(
|
||||||
expand_action_name,
|
expand_action_name,
|
||||||
ToggleCollapse {
|
ToggleCollapse {
|
||||||
|
@ -2098,7 +2120,7 @@ impl CollabPanel {
|
||||||
channel_id: location.channel,
|
channel_id: location.channel,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
]);
|
||||||
|
|
||||||
if self.channel_store.read(cx).is_user_admin(location.channel) {
|
if self.channel_store.read(cx).is_user_admin(location.channel) {
|
||||||
let parent_id = location.path.parent_id();
|
let parent_id = location.path.parent_id();
|
||||||
|
@ -2120,25 +2142,27 @@ impl CollabPanel {
|
||||||
ContextMenuItem::Separator,
|
ContextMenuItem::Separator,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if let Some(parent) = parent_id {
|
items.push(ContextMenuItem::action(
|
||||||
items.push(ContextMenuItem::action(
|
if parent_id.is_some() {
|
||||||
"Unlink from parent",
|
"Unlink from parent"
|
||||||
UnlinkChannel {
|
} else {
|
||||||
channel_id: location.channel,
|
"Unlink from root"
|
||||||
parent_id: parent,
|
},
|
||||||
},
|
UnlinkChannel {
|
||||||
))
|
channel_id: location.channel,
|
||||||
}
|
parent_id,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
items.extend([
|
items.extend([
|
||||||
ContextMenuItem::action(
|
ContextMenuItem::action(
|
||||||
"Link to new parent",
|
"Link this channel",
|
||||||
LinkChannel {
|
LinkChannel {
|
||||||
channel_id: location.channel,
|
channel_id: location.channel,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ContextMenuItem::action(
|
ContextMenuItem::action(
|
||||||
"Move",
|
"Move this channel",
|
||||||
MoveChannel {
|
MoveChannel {
|
||||||
channel_id: location.channel,
|
channel_id: location.channel,
|
||||||
parent_id,
|
parent_id,
|
||||||
|
@ -2146,15 +2170,6 @@ impl CollabPanel {
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if let Some(copy_channel) = copy_channel {
|
|
||||||
items.push(ContextMenuItem::action(
|
|
||||||
format!("Put '#{}'", copy_channel),
|
|
||||||
PutChannel {
|
|
||||||
to: location.channel,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
items.extend([
|
items.extend([
|
||||||
ContextMenuItem::Separator,
|
ContextMenuItem::Separator,
|
||||||
ContextMenuItem::action(
|
ContextMenuItem::action(
|
||||||
|
|
|
@ -167,7 +167,9 @@ message Envelope {
|
||||||
GetChannelMessagesResponse get_channel_messages_response = 149;
|
GetChannelMessagesResponse get_channel_messages_response = 149;
|
||||||
RemoveChannelMessage remove_channel_message = 150;
|
RemoveChannelMessage remove_channel_message = 150;
|
||||||
|
|
||||||
MoveChannel move_channel = 151; // Current max
|
LinkChannel link_channel = 151;
|
||||||
|
UnlinkChannel unlink_channel = 152;
|
||||||
|
MoveChannel move_channel = 153; // Current max
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1082,10 +1084,20 @@ message GetChannelMessagesResponse {
|
||||||
bool done = 2;
|
bool done = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message LinkChannel {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
uint64 to = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UnlinkChannel {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
optional uint64 from = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message MoveChannel {
|
message MoveChannel {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
optional uint64 from_parent = 2;
|
optional uint64 from = 2;
|
||||||
optional uint64 to = 3;
|
uint64 to = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message JoinChannelBuffer {
|
message JoinChannelBuffer {
|
||||||
|
|
|
@ -248,6 +248,8 @@ messages!(
|
||||||
(UpdateContacts, Foreground),
|
(UpdateContacts, Foreground),
|
||||||
(DeleteChannel, Foreground),
|
(DeleteChannel, Foreground),
|
||||||
(MoveChannel, Foreground),
|
(MoveChannel, Foreground),
|
||||||
|
(LinkChannel, Foreground),
|
||||||
|
(UnlinkChannel, Foreground),
|
||||||
(UpdateChannels, Foreground),
|
(UpdateChannels, Foreground),
|
||||||
(UpdateDiagnosticSummary, Foreground),
|
(UpdateDiagnosticSummary, Foreground),
|
||||||
(UpdateFollowers, Foreground),
|
(UpdateFollowers, Foreground),
|
||||||
|
@ -332,6 +334,8 @@ request_messages!(
|
||||||
(DeleteChannel, Ack),
|
(DeleteChannel, Ack),
|
||||||
(RenameProjectEntry, ProjectEntryResponse),
|
(RenameProjectEntry, ProjectEntryResponse),
|
||||||
(RenameChannel, ChannelResponse),
|
(RenameChannel, ChannelResponse),
|
||||||
|
(LinkChannel, Ack),
|
||||||
|
(UnlinkChannel, Ack),
|
||||||
(MoveChannel, Ack),
|
(MoveChannel, Ack),
|
||||||
(SaveBuffer, BufferSaved),
|
(SaveBuffer, BufferSaved),
|
||||||
(SearchProject, SearchProjectResponse),
|
(SearchProject, SearchProjectResponse),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue