Fix navigation issue in connect and allow leaving channels (#635)

* Fix navigation issue in connect and allow leaving channels

* Fix chat channel memberships request spec
This commit is contained in:
Ben Halpern 2018-07-30 18:40:30 -04:00 committed by GitHub
parent a3788dfbd1
commit 57d1bc48d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 191 additions and 75 deletions

View file

@ -28,6 +28,16 @@ class ChatChannelMembershipsController < ApplicationController
render "chat_channels/index.json"
end
def destroy
@chat_channel_membership = ChatChannel.find(params[:id]).
chat_channel_memberships.where(user_id: current_user.id).first
authorize @chat_channel_membership
@chat_channel_membership.update(status: "left_channel")
@chat_channel_membership.chat_channel.index!
@chat_channels_memberships = []
render json: { result: "left channel" }, status: 201
end
def permitted_params
params.require(:chat_channel_membership).permit(:user_id, :chat_channel_id, :user_action, :id)
end

View file

@ -1,20 +1,21 @@
import { h, Component } from 'preact';
const algoliaId = document.querySelector("meta[name='algolia-public-id']").content
const algoliaKey = document.querySelector("meta[name='algolia-public-key']").content
const env = document.querySelector("meta[name='environment']").content
const algoliaId = document.querySelector("meta[name='algolia-public-id']")
.content;
const algoliaKey = document.querySelector("meta[name='algolia-public-key']")
.content;
const env = document.querySelector("meta[name='environment']").content;
const client = algoliasearch(algoliaId, algoliaKey);
const index = client.initIndex('searchables_'+env);
const index = client.initIndex(`searchables_${ env}`);
export default class ChannelDetails extends Component {
constructor(props) {
super(props);
this.state = {
searchedUsers: [],
invitations: []
}
invitations: [],
hasLeftChannel: false,
};
}
triggerUserSearch = e => {
@ -22,28 +23,23 @@ export default class ChannelDetails extends Component {
const query = e.target.value;
const filters = {
hitsPerPage: 20,
attributesToRetrieve: [
'id',
'title',
'path'
],
attributesToRetrieve: ['id', 'title', 'path'],
attributesToHighlight: [],
filters: 'class_name:User',
}
};
if (query.length > 0) {
index.search(query, filters)
.then(function(content) {
component.setState({searchedUsers:content.hits})
index.search(query, filters).then((content) => {
component.setState({ searchedUsers: content.hits });
});
} else {
component.setState({searchedUsers:[]})
component.setState({ searchedUsers: [] });
}
}
};
triggerInvite = e => {
const component = this;
const id = e.target.dataset.content;
e.target.style.display = 'none'
e.target.style.display = 'none';
fetch(`/chat_channel_memberships`, {
method: 'POST',
headers: {
@ -55,64 +51,131 @@ export default class ChannelDetails extends Component {
chat_channel_membership: {
user_id: id,
chat_channel_id: component.props.channel.id,
}
},
}),
credentials: 'same-origin',
})
.then(response => response)
.then(this.handleInvitationSuccess)
.catch(null);
}
};
triggerLeaveChannel = e => {
e.preventDefault();
const id = e.target.dataset.content;
fetch(`/chat_channel_memberships/${id}`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'X-CSRF-Token': window.csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
credentials: 'same-origin',
})
.then(response => response)
.then(this.handleLeaveChannelSuccess)
.catch(null);
};
handleLeaveChannelSuccess = response => {
this.setState({ hasLeftChannel: true });
};
handleInvitationSuccess = response => {
console.log(response)
}
console.log(response);
};
render() {
const channel = this.props.channel
const users = Object.values(channel.channel_users).map((user) => {
return <div>
const channel = this.props.channel;
const users = Object.values(channel.channel_users).map(user => (
<div>
<a
href={'/'+user.username}
style={{color:user.darker_color,padding: "3px 0px"}}
href={`/${ user.username}`}
style={{ color: user.darker_color, padding: '3px 0px' }}
data-content={`users/by_username?url=${user.username}`}
>
>
{user.name}
</a>
</div>
});
let subHeader = ''
));
let subHeader = '';
if (users.length === 80) {
subHeader = <h3>Recently Active Members</h3>
subHeader = <h3>Recently Active Members</h3>;
}
let modSection = ''
let modSection = '';
let searchedUsers = [];
let pendingInvites = [];
if (channel.channel_mod_ids.includes(window.currentUser.id)) {
searchedUsers = this.state.searchedUsers.map((user) => {
return <div><a href={user.path} target='_blank'>{user.title}</a> <button onClick={this.triggerInvite} data-content={user.id}>Invite</button></div>
})
pendingInvites = channel.pending_users_select_fields.map((user) => {
return <div><a href={'/'+user.username} target='_blank' data-content={'users/'+user.id}>@{user.username} - {user.name}</a></div>
})
modSection = <div>
<h2>Invite Members</h2>
<input onKeyUp={this.triggerUserSearch} placeholder="Find users"/>
{searchedUsers}
<h2>Pending Invites:</h2>
{pendingInvites}
<div style={{marginTop: "10px"}}>All functionality is early beta. Contact us if you need help with anything.</div>
</div>
}
return <div>
<h1>{channel.channel_name}</h1>
{subHeader}
<div style={{marginBottom: '20px'}}>
<em>{channel.description || ''}</em>
</div>
{users}
{modSection}
</div>
searchedUsers = this.state.searchedUsers.map(user => (
<div>
<a href={user.path} target="_blank">
{user.title}
</a>{' '}
<button onClick={this.triggerInvite} data-content={user.id}>
Invite
</button>
</div>
));
pendingInvites = channel.pending_users_select_fields.map(user => (
<div>
<a
href={`/${ user.username}`}
target="_blank"
data-content={`users/${ user.id}`}
>
@{user.username} - {user.name}
</a>
</div>
));
modSection = (
<div>
<h2>Invite Members</h2>
<input onKeyUp={this.triggerUserSearch} placeholder="Find users" />
{searchedUsers}
<h2>Pending Invites:</h2>
{pendingInvites}
<div style={{ marginTop: '10px' }}>
All functionality is early beta. Contact us if you need help with
anything.
</div>
</div>
);
} else if (this.state.hasLeftChannel) {
modSection = (
<div>
<h2>Danger Zone</h2>
<h3>You have left this channel 😢😢😢</h3>
<h4>It may not be immediately in the sidebar</h4>
<p>
Contact the admins at <a href="mailto:yo@dev.to">yo@dev.to</a> if
this was a mistake
</p>
</div>
);
} else {
modSection = (
<div>
<h2>Danger Zone</h2>
<button
onClick={this.triggerLeaveChannel}
data-content={channel.id}
>
Leave Channel.
</button>
</div>
);
}
return (
<div>
<h1>{channel.channel_name}</h1>
{subHeader}
<div style={{ marginBottom: '20px' }}>
<em>{channel.description || ''}</em>
</div>
{users}
{modSection}
</div>
);
}
}
}

View file

@ -10,18 +10,29 @@ HTMLDocument.prototype.ready = new Promise(resolve => {
return null;
});
document.ready.then(
loadChat(); // Load initially
window.InstantClick.on('change', () => {
loadChat(); // Load via instantclick nav
});
function loadChat() {
getUserDataAndCsrfToken().then(currentUser => {
if (document.getElementById('chat')) {
const { chatChannels, pusherKey, chatOptions, algoliaKey, algoliaId, algoliaIndex, githubToken } = document.getElementById(
'chat',
).dataset;
const {
chatChannels,
pusherKey,
chatOptions,
algoliaKey,
algoliaId,
algoliaIndex,
githubToken,
} = document.getElementById('chat').dataset;
window.currentUser = currentUser;
window.csrfToken = document.querySelector(
"meta[name='csrf-token']",
).content;
const renderTarget = document.getElementById('chat');
const placeholder = document.getElementById('chat_placeholder')
const placeholder = document.getElementById('chat_placeholder');
const root = render(
<Chat
pusherKey={pusherKey}
@ -32,14 +43,29 @@ document.ready.then(
algoliaIndex={algoliaIndex}
githubToken={githubToken}
/>,
renderTarget, renderTarget.firstChild
renderTarget,
renderTarget.firstChild,
);
if (placeholder) {
renderTarget.removeChild(placeholder);
}
window.InstantClick.on('change', () => {
render('', document.body, root);
if (window.location.href.indexOf('/connect') != -1) {
const root = render(
<Chat
pusherKey={pusherKey}
chatChannels={chatChannels}
chatOptions={chatOptions}
algoliaId={algoliaId}
algoliaKey={algoliaKey}
algoliaIndex={algoliaIndex}
githubToken={githubToken}
/>,
renderTarget,
renderTarget.firstChild,
);
}
});
}
}),
);
});
}

View file

@ -1,6 +1,9 @@
class ChatChannelMembershipPolicy < ApplicationPolicy
def update?
record.present? && user.id == record.user_id
end
end
def destroy?
record.present? && user.id == record.user_id
end
end

View file

@ -73,7 +73,7 @@ Rails.application.routes.draw do
resources :messages, only: [:create]
resources :chat_channels, only: [:index, :show, :create, :update]
resources :chat_channel_memberships, only: [ :create, :update]
resources :chat_channel_memberships, only: [ :create, :update, :destroy]
resources :articles, only: [:update,:create,:destroy]
resources :comments, only: [:create,:update,:destroy]
resources :users, only: [:update]

View file

@ -15,13 +15,13 @@ RSpec.describe ChatChannelMembershipPolicy do
let(:user) { create(:user) }
let(:chat_channel) { create(:chat_channel ) }
let(:chat_channel_membership) { create(:chat_channel_membership, user_id: user.id, chat_channel_id: chat_channel.id ) }
it { is_expected.to permit_actions(%i[update]) }
it { is_expected.to permit_actions(%i[update destroy]) }
end
context "when user does not belong to membership" do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:chat_channel) { create(:chat_channel ) }
let(:chat_channel_membership) { create(:chat_channel_membership, user_id: other_user.id, chat_channel_id: chat_channel.id ) }
it { is_expected.to forbid_actions(%i[update]) }
it { is_expected.to forbid_actions(%i[update destroy]) }
end
end

View file

@ -61,4 +61,18 @@ RSpec.describe "ChatChannelMemberships", type: :request do
end.to raise_error(Pundit::NotAuthorizedError)
end
end
end
describe "DELETE /chat_channel_memberships/:id" do
before do
user.add_role(:super_admin)
post "/chat_channel_memberships", params: {
chat_channel_membership: {user_id: second_user.id, chat_channel_id: chat_channel.id}}
end
it "leaves chat channel" do
membership = ChatChannelMembership.last
sign_in second_user
delete "/chat_channel_memberships/#{membership.chat_channel.id}", params: {}
expect(ChatChannelMembership.find(membership.id).status).to eq("left_channel")
end
end
end