Skip to content
Snippets Groups Projects
Unverified Commit d80b758d authored by Ahmad Farhat's avatar Ahmad Farhat Committed by GitHub
Browse files

Add invited tab (#4031)

* Add invited tab

* Add pagination

* CR
parent a09901ab
No related branches found
No related tags found
No related merge requests found
...@@ -4,10 +4,20 @@ module Api ...@@ -4,10 +4,20 @@ module Api
module V1 module V1
module Admin module Admin
class InvitationsController < ApiController class InvitationsController < ApiController
before_action only: %i[create] do before_action only: %i[index create] do
ensure_authorized('ManageUsers') ensure_authorized('ManageUsers')
end end
# GET /api/v1/admin/invitations
def index
sort_config = config_sorting(allowed_columns: %w[email])
invitations = Invitation.where(provider: current_provider)&.order(sort_config, updated_at: :desc)&.search(params[:search])
pagy, invitations = pagy(invitations)
render_data data: invitations, meta: pagy_metadata(pagy), status: :ok
end
# POST /api/v1/admin/invitations # POST /api/v1/admin/invitations
def create def create
params[:invitations][:emails].split(',').each do |email| params[:invitations][:emails].split(',').each do |email|
......
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Table } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { CheckIcon, XMarkIcon } from '@heroicons/react/24/solid';
import SortBy from '../../shared_components/search/SortBy';
import useInvitations from '../../../hooks/queries/admin/manage_users/useInvitations';
import Pagination from '../../shared_components/Pagination';
export default function InvitedUsers({ searchInput }) {
const { t } = useTranslation();
const [page, setPage] = useState();
const { data: invitations } = useInvitations(searchInput, page);
return (
<div id="admin-table">
<Table className="table-bordered border border-2 mb-0" hover>
<thead>
<tr className="text-muted small">
<th className="fw-normal border-end-0">{ t('user.email_address') }<SortBy fieldName="email" /></th>
<th className="fw-normal border-0">{ t('admin.manage_users.invited.time_sent') }</th>
<th className="fw-normal border-0">{ t('admin.manage_users.invited.valid') }</th>
</tr>
</thead>
<tbody className="border-top-0">
{invitations?.data?.length
? (
invitations?.data?.map((invitation) => (
<tr key={invitation.email} className="align-middle text-muted">
<td className="text-dark border-0">{invitation.email}</td>
<td className="text-dark border-0">{invitation.updated_at}</td>
<td className="text-dark border-0">
{ invitation.valid ? <CheckIcon className="text-success hi-s" /> : <XMarkIcon className="text-danger hi-s" />}
</td>
</tr>
))
)
: (
<tr>
<td className="fw-bold">
{ t('user.no_user_found') }
</td>
</tr>
)}
</tbody>
</Table>
<div className="pagination-wrapper">
<Pagination
page={invitations?.meta?.page}
totalPages={invitations?.meta?.pages}
setPage={setPage}
/>
</div>
</div>
);
}
InvitedUsers.propTypes = {
searchInput: PropTypes.string,
};
InvitedUsers.defaultProps = {
searchInput: '',
};
...@@ -12,6 +12,7 @@ import UserSignupForm from './forms/UserSignupForm'; ...@@ -12,6 +12,7 @@ import UserSignupForm from './forms/UserSignupForm';
import useSiteSetting from '../../../hooks/queries/site_settings/useSiteSetting'; import useSiteSetting from '../../../hooks/queries/site_settings/useSiteSetting';
import SearchBar from '../../shared_components/search/SearchBar'; import SearchBar from '../../shared_components/search/SearchBar';
import InviteUserForm from './forms/InviteUserForm'; import InviteUserForm from './forms/InviteUserForm';
import InvitedUsers from './InvitedUsers';
export default function ManageUsers() { export default function ManageUsers() {
const { t } = useTranslation(); const { t } = useTranslation();
...@@ -78,8 +79,8 @@ export default function ManageUsers() { ...@@ -78,8 +79,8 @@ export default function ManageUsers() {
</Tab> </Tab>
{ registrationMethod { registrationMethod
&& ( && (
<Tab eventKey="invited" title={t('admin.manage_users.invited')}> <Tab eventKey="invited" title={t('admin.manage_users.invited_tab')}>
Invited users component <InvitedUsers input={searchInput} />
</Tab> </Tab>
)} )}
</Tabs> </Tabs>
......
import { useQuery } from 'react-query';
import { useSearchParams } from 'react-router-dom';
import axios from '../../../../helpers/Axios';
export default function useInvitations(input, page) {
const [searchParams] = useSearchParams();
const params = {
'sort[column]': searchParams.get('sort[column]'),
'sort[direction]': searchParams.get('sort[direction]'),
search: input,
page,
};
return useQuery(
['getInvitations', { ...params }],
() => axios.get('/admin/invitations.json', { params }).then((resp) => resp.data),
{
keepPreviousData: true,
},
);
}
# frozen_string_literal: true # frozen_string_literal: true
class Invitation < ApplicationRecord class Invitation < ApplicationRecord
INVITATION_VALIDITY_PERIOD = 48.hours
has_secure_token :token has_secure_token :token
validates :email, presence: true, uniqueness: { scope: :provider } validates :email, presence: true, uniqueness: { scope: :provider }
validates :provider, presence: true validates :provider, presence: true
validates :token, uniqueness: true validates :token, uniqueness: true
def self.search(input)
return where('email ILIKE ?', "%#{input}%") if input
all
end
end end
...@@ -4,4 +4,8 @@ class ApplicationSerializer < ActiveModel::Serializer ...@@ -4,4 +4,8 @@ class ApplicationSerializer < ActiveModel::Serializer
def created_at def created_at
object.created_at.strftime('%A %B %e, %Y %l:%M%P') object.created_at.strftime('%A %B %e, %Y %l:%M%P')
end end
def updated_at
object.updated_at.strftime('%A %B %e, %Y %l:%M%P')
end
end end
# frozen_string_literal: true
class InvitationSerializer < ApplicationSerializer
attributes :email, :updated_at, :valid
def valid
object.updated_at.in(Invitation::INVITATION_VALIDITY_PERIOD)
end
end
...@@ -77,7 +77,7 @@ Rails.application.routes.draw do ...@@ -77,7 +77,7 @@ Rails.application.routes.draw do
resources :site_settings, only: %i[index update], param: :name resources :site_settings, only: %i[index update], param: :name
resources :rooms_configurations, only: :update, param: :name resources :rooms_configurations, only: :update, param: :name
resources :roles resources :roles
resources :invitations, only: :create resources :invitations, only: %i[index create]
# TODO: Review update route # TODO: Review update route
resources :role_permissions, only: [:index] do resources :role_permissions, only: [:index] do
collection do collection do
......
...@@ -157,7 +157,7 @@ ...@@ -157,7 +157,7 @@
"pending": "Pending", "pending": "Pending",
"banned": "Banned", "banned": "Banned",
"deleted": "Deleted", "deleted": "Deleted",
"invited": "Invited", "invited_tab": "Invited",
"invite_user": "Invite User", "invite_user": "Invite User",
"send_invitation": "Send Invitation", "send_invitation": "Send Invitation",
"new_user": "New User", "new_user": "New User",
...@@ -173,7 +173,11 @@ ...@@ -173,7 +173,11 @@
"enter_user_email": "Enter an email", "enter_user_email": "Enter an email",
"enter_user_name": "Enter a name", "enter_user_name": "Enter a name",
"are_you_sure_delete_account": "Are you sure you want to delete {{user.name}}'s account?", "are_you_sure_delete_account": "Are you sure you want to delete {{user.name}}'s account?",
"delete_account_warning": "If you choose to delete this account, it will NOT be recoverable." "delete_account_warning": "If you choose to delete this account, it will NOT be recoverable.",
"invited": {
"time_sent": "Time Sent",
"valid": "Valid"
}
}, },
"server_rooms": { "server_rooms": {
"server_rooms": "Server Rooms", "server_rooms": "Server Rooms",
......
...@@ -12,6 +12,46 @@ RSpec.describe Api::V1::Admin::InvitationsController, type: :controller do ...@@ -12,6 +12,46 @@ RSpec.describe Api::V1::Admin::InvitationsController, type: :controller do
sign_in_user(user_with_manage_users_permission) sign_in_user(user_with_manage_users_permission)
end end
describe 'invitation#index' do
it 'returns the list of invitations' do
invitations = [
create(:invitation, email: 'user@test.com'),
create(:invitation, email: 'user2@test.com'),
create(:invitation, email: 'user3@test.com')
]
get :index
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)['data'].pluck('email')).to match_array(invitations.pluck(:email))
end
it 'returns the invitations according to the query' do
invitations = [
create(:invitation, email: 'user@test.com'),
create(:invitation, email: 'user2@test.com')
]
create(:invitation, email: 'user3@not.com')
get :index, params: { search: 'test.com' }
expect(JSON.parse(response.body)['data'].pluck('email')).to match_array(invitations.pluck(:email))
end
context 'user without ManageUsers permission' do
before do
sign_in_user(user)
end
it 'returns :forbidden for user without ManageUsers permission' do
valid_params = { emails: 'user@test.com,user2@test.com,user3@test.com' }
expect { post :create, params: { invitations: valid_params } }.not_to change(Role, :count)
expect(response).to have_http_status(:forbidden)
end
end
end
describe 'invitation#create' do describe 'invitation#create' do
it 'returns :ok and creates the invitations' do it 'returns :ok and creates the invitations' do
valid_params = { emails: 'user@test.com,user2@test.com,user3@test.com' } valid_params = { emails: 'user@test.com,user2@test.com,user3@test.com' }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment