feat: Add APIs to create custom views on the dashboard (#2498)

This commit is contained in:
Pranav Raj S 2021-06-29 19:29:57 +05:30 committed by GitHub
parent fa37f8e185
commit 30832d8a34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 654 additions and 9 deletions

View file

@ -0,0 +1,50 @@
class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseController
protect_from_forgery with: :null_session
before_action :fetch_custom_filters, except: [:create]
before_action :fetch_custom_filter, only: [:show, :update, :destroy]
DEFAULT_FILTER_TYPE = 'conversation'.freeze
def index; end
def show; end
def create
@custom_filter = current_user.custom_filters.create!(
permitted_payload.merge(account_id: Current.account.id)
)
end
def update
@custom_filter.update!(permitted_payload)
end
def destroy
@custom_filter.destroy
head :no_content
end
private
def fetch_custom_filters
@custom_filters = current_user.custom_filters.where(
account_id: Current.account.id,
filter_type: permitted_params[:filter_type] || DEFAULT_FILTER_TYPE
)
end
def fetch_custom_filter
@custom_filter = @custom_filters.find(permitted_params[:id])
end
def permitted_payload
params.require(:custom_filter).permit(
:name,
:filter_type,
query: {}
)
end
def permitted_params
params.permit(:id, :filter_type)
end
end

View file

@ -59,6 +59,7 @@ class Account < ApplicationRecord
has_many :kbase_categories, dependent: :destroy, class_name: '::Kbase::Category' has_many :kbase_categories, dependent: :destroy, class_name: '::Kbase::Category'
has_many :kbase_articles, dependent: :destroy, class_name: '::Kbase::Article' has_many :kbase_articles, dependent: :destroy, class_name: '::Kbase::Article'
has_many :teams, dependent: :destroy has_many :teams, dependent: :destroy
has_many :custom_filters, dependent: :destroy
has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING) has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h

View file

@ -0,0 +1,24 @@
# == Schema Information
#
# Table name: custom_filters
#
# id :bigint not null, primary key
# filter_type :integer default("conversation"), not null
# name :string not null
# query :jsonb not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_custom_filters_on_account_id (account_id)
# index_custom_filters_on_user_id (user_id)
#
class CustomFilter < ApplicationRecord
belongs_to :user
belongs_to :account
enum filter_type: { conversation: 0, contact: 1, report: 2 }
end

View file

@ -83,6 +83,7 @@ class User < ApplicationRecord
has_many :team_members, dependent: :destroy has_many :team_members, dependent: :destroy
has_many :teams, through: :team_members has_many :teams, through: :team_members
has_many :notes, dependent: :nullify has_many :notes, dependent: :nullify
has_many :custom_filters, dependent: :destroy
before_validation :set_password_and_uid, on: :create before_validation :set_password_and_uid, on: :create

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/custom_filter.json.jbuilder', resource: @custom_filter

View file

@ -0,0 +1,3 @@
json.array! @custom_filters do |custom_filter|
json.partial! 'api/v1/models/custom_filter.json.jbuilder', resource: custom_filter
end

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/custom_filter.json.jbuilder', resource: @custom_filter

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/custom_filter.json.jbuilder', resource: @custom_filter

View file

@ -0,0 +1,6 @@
json.id resource.id
json.name resource.name
json.filter_type resource.filter_type
json.query resource.query
json.created_at resource.created_at
json.updated_at resource.updated_at

View file

@ -87,7 +87,7 @@ Rails.application.routes.draw do
resources :labels, only: [:create, :index] resources :labels, only: [:create, :index]
end end
end end
resources :custom_filters, only: [:index, :show, :create, :update, :destroy]
resources :inboxes, only: [:index, :create, :update, :destroy] do resources :inboxes, only: [:index, :create, :update, :destroy] do
get :assignable_agents, on: :member get :assignable_agents, on: :member
get :campaigns, on: :member get :campaigns, on: :member

View file

@ -0,0 +1,12 @@
class CreateCustomFilters < ActiveRecord::Migration[6.0]
def change
create_table :custom_filters do |t|
t.string :name, null: false
t.integer :filter_type, null: false, default: 0
t.jsonb :query, null: false, default: '{}'
t.references :account, index: true, null: false
t.references :user, index: true, null: false
t.timestamps
end
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_06_18_095823) do ActiveRecord::Schema.define(version: 2021_06_23_150613) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
@ -267,6 +267,18 @@ ActiveRecord::Schema.define(version: 2021_06_18_095823) do
t.index ["team_id"], name: "index_conversations_on_team_id" t.index ["team_id"], name: "index_conversations_on_team_id"
end end
create_table "custom_filters", force: :cascade do |t|
t.string "name", null: false
t.integer "filter_type", default: 0, null: false
t.jsonb "query", default: "{}", null: false
t.bigint "account_id", null: false
t.bigint "user_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["account_id"], name: "index_custom_filters_on_account_id"
t.index ["user_id"], name: "index_custom_filters_on_user_id"
end
create_table "data_imports", force: :cascade do |t| create_table "data_imports", force: :cascade do |t|
t.bigint "account_id", null: false t.bigint "account_id", null: false
t.string "data_type", null: false t.string "data_type", null: false

View file

@ -0,0 +1,146 @@
require 'rails_helper'
RSpec.describe 'Custom Filters API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/custom_filters' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_filters"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:user) { create(:user, account: account) }
let!(:custom_filter) { create(:custom_filter, user: user, account: account) }
it 'returns all custom_filter related to the user' do
get "/api/v1/accounts/#{account.id}/custom_filters",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_body = JSON.parse(response.body)
expect(response_body.first['name']).to eq(custom_filter.name)
expect(response_body.first['query']).to eq(custom_filter.query)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/custom_filters/:id' do
let(:user) { create(:user, account: account) }
let!(:custom_filter) { create(:custom_filter, user: user, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'shows the custom filter' do
get "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(custom_filter.name)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/custom_filters' do
let(:payload) { { custom_filter: { name: 'vip-customers', filter_type: 'conversation', query: { labels: ['vip-customers'] } } } }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
expect { post "/api/v1/accounts/#{account.id}/custom_filters", params: payload }.to change(CustomFilter, :count).by(0)
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:user) { create(:user, account: account) }
it 'creates the filter' do
expect do
post "/api/v1/accounts/#{account.id}/custom_filters", headers: user.create_new_auth_token,
params: payload
end.to change(CustomFilter, :count).by(1)
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['name']).to eq 'vip-customers'
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/custom_filters/:id' do
let(:payload) { { custom_filter: { name: 'vip-customers', filter_type: 'contact', query: { labels: ['vip-customers'] } } } }
let(:user) { create(:user, account: account) }
let!(:custom_filter) { create(:custom_filter, user: user, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}",
params: payload
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
it 'updates the custom filter' do
patch "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}",
headers: user.create_new_auth_token,
params: payload,
as: :json
expect(response).to have_http_status(:success)
expect(custom_filter.reload.name).to eq('vip-customers')
expect(custom_filter.reload.filter_type).to eq('contact')
expect(custom_filter.reload.query['labels']).to eq(['vip-customers'])
end
it 'prevents the update of custom filter of another user/account' do
other_account = create(:account)
other_user = create(:user, account: other_account)
other_custom_filter = create(:custom_filter, user: other_user, account: other_account)
patch "/api/v1/accounts/#{account.id}/custom_filters/#{other_custom_filter.id}",
headers: user.create_new_auth_token,
params: payload,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/custom_filters/:id' do
let(:user) { create(:user, account: account) }
let!(:custom_filter) { create(:custom_filter, user: user, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated admin user' do
it 'deletes custom filter if it is attached to the current user and account' do
delete "/api/v1/accounts/#{account.id}/custom_filters/#{custom_filter.id}",
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:no_content)
expect(user.custom_filters.count).to be 0
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
FactoryBot.define do
factory :custom_filter do
sequence(:name) { |n| "Custom Filter #{n}" }
filter_type { 0 }
query { { labels: ['customer-support'], status: 'open' } }
user
account
end
end

View file

@ -28,6 +28,8 @@ contact_inboxes:
$ref: ./resource/contact_inboxes.yml $ref: ./resource/contact_inboxes.yml
contactable_inboxes: contactable_inboxes:
$ref: ./resource/contactable_inboxes.yml $ref: ./resource/contactable_inboxes.yml
custom_filter:
$ref: ./resource/custom_filter.yml
account: account:
$ref: ./resource/account.yml $ref: ./resource/account.yml
platform_account: platform_account:
@ -74,6 +76,10 @@ conversation_message_create:
team_create_update_payload: team_create_update_payload:
$ref: ./request/team/create_update_payload.yml $ref: ./request/team/create_update_payload.yml
# Custom Filter request Payload
custom_filter_create_update_payload:
$ref: ./request/custom_filter/create_update_payload.yml
integrations_hook_create_payload: integrations_hook_create_payload:
$ref: ./request/integrations/hook_create_payload.yml $ref: ./request/integrations/hook_create_payload.yml

View file

@ -0,0 +1,12 @@
type: object
properties:
name:
type: string
description: The name of the custom filter
type:
type: string
enum: ["conversation", "contact", "report"]
description: The description about the custom filter
query:
type: object
description: A query that needs to be saved as a custom filter

View file

@ -0,0 +1,21 @@
type: object
properties:
id:
type: number
description: The ID of the custom filter
name:
type: string
description: The name of the custom filter
type:
type: string
enum: ["conversation", "contact", "report"]
description: The description about the custom filter
query:
type: object
description: A query that needs to be saved as a custom filter
created_at:
type: datetime
description: The time at which the custom filter was created
updated_at:
type: datetime
description: The time at which the custom filter was updated

View file

@ -62,6 +62,7 @@ x-tagGroups:
- Integrations - Integrations
- Profile - Profile
- Teams - Teams
- Custom Filter
- name: Public - name: Public
tags: tags:
- Contacts API - Contacts API

View file

@ -0,0 +1,6 @@
in: path
name: custom_filter_id
schema:
type: integer
required: true
description: The numeric ID of the custom filter

View file

@ -28,6 +28,8 @@ page:
platform_user_id: platform_user_id:
$ref: ./platform_user_id.yml $ref: ./platform_user_id.yml
custom_filter_id:
$ref: ./custom_filter_id.yml
public_inbox_identifier: public_inbox_identifier:
$ref: ./public/inbox_identifier.yml $ref: ./public/inbox_identifier.yml

View file

@ -0,0 +1,19 @@
tags:
- Custom Filter
operationId: create-a-custom-filter
summary: Create a custom filter
description: Create a custom filter in the account
parameters:
- $ref: '#/parameters/account_id'
- name: data
in: body
required: true
schema:
$ref: '#/definitions/custom_filter_create_update_payload'
responses:
200:
description: Success
schema:
$ref: '#/definitions/custom_filter'
401:
description: Unauthorized

View file

@ -0,0 +1,12 @@
tags:
- Custom Filter
operationId: delete-a-custom-filter
summary: Delete a custom filter
description: Delete a custom filter from the account
responses:
200:
description: Success
401:
description: Unauthorized
404:
description: The custom filter does not exist in the account

View file

@ -0,0 +1,15 @@
tags:
- Custom Filter
operationId: list-all-filters
summary: List all custom filters
description: List all custom filters in a category of a user
responses:
200:
description: Success
schema:
type: array
description: 'Array of custom filters'
items:
$ref: '#/definitions/custom_filter'
401:
description: Unauthorized

View file

@ -0,0 +1,14 @@
tags:
- Custom Filter
operationId: get-details-of-a-single-custom-filter
summary: Get a custom filter details
description: Get the details of a custom filter in the account
responses:
200:
description: Success
schema:
$ref: '#/definitions/custom_filter'
401:
description: Unauthorized
404:
description: The given team ID does not exist in the account

View file

@ -0,0 +1,18 @@
tags:
- Custom Filter
operationId: update-a-custom-filter
summary: Update a custom filter
description: Update a custom filter's attributes
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/custom_filter_create_update_payload'
responses:
200:
description: Success
schema:
$ref: '#/definitions/custom_filter'
401:
description: Unauthorized

View file

@ -264,3 +264,31 @@ public/api/v1/inboxes/{inbox_identifier}/contacts/{contact_identifier}/conversat
$ref: ./teams/update.yml $ref: ./teams/update.yml
delete: delete:
$ref: ./teams/delete.yml $ref: ./teams/delete.yml
### Custom Filters
# Teams
/api/v1/accounts/{account_id}/custom_filters:
parameters:
- $ref: '#/parameters/account_id'
- in: query
name: filter_type
schema:
type: string
enum: ['conversation', 'contact', 'report']
required: false
description: The type of custom filter
get:
$ref: ./custom_filters/index.yml
post:
$ref: ./custom_filters/create.yml
/api/v1/accounts/{account_id}/custom_filters/{custom_filter_id}:
parameters:
- $ref: '#/parameters/account_id'
- $ref: '#/parameters/custom_filter_id'
get:
$ref: './custom_filters/show.yml'
patch:
$ref: ./custom_filters/update.yml
delete:
$ref: ./custom_filters/delete.yml

View file

@ -2527,6 +2527,162 @@
} }
} }
} }
},
"/api/v1/accounts/{account_id}/custom_filters": {
"parameters": [
{
"$ref": "#/parameters/account_id"
},
{
"in": "query",
"name": "filter_type",
"schema": {
"type": "string",
"enum": [
"conversation",
"contact",
"report"
]
},
"required": false,
"description": "The type of custom filter"
}
],
"get": {
"tags": [
"Custom Filter"
],
"operationId": "list-all-filters",
"summary": "List all custom filters",
"description": "List all custom filters in a category of a user",
"responses": {
"200": {
"description": "Success",
"schema": {
"type": "array",
"description": "Array of custom filters",
"items": {
"$ref": "#/definitions/custom_filter"
}
}
},
"401": {
"description": "Unauthorized"
}
}
},
"post": {
"tags": [
"Custom Filter"
],
"operationId": "create-a-custom-filter",
"summary": "Create a custom filter",
"description": "Create a custom filter in the account",
"parameters": [
{
"$ref": "#/parameters/account_id"
},
{
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/custom_filter_create_update_payload"
}
}
],
"responses": {
"200": {
"description": "Success",
"schema": {
"$ref": "#/definitions/custom_filter"
}
},
"401": {
"description": "Unauthorized"
}
}
}
},
"/api/v1/accounts/{account_id}/custom_filters/{custom_filter_id}": {
"parameters": [
{
"$ref": "#/parameters/account_id"
},
{
"$ref": "#/parameters/custom_filter_id"
}
],
"get": {
"tags": [
"Custom Filter"
],
"operationId": "get-details-of-a-single-custom-filter",
"summary": "Get a custom filter details",
"description": "Get the details of a custom filter in the account",
"responses": {
"200": {
"description": "Success",
"schema": {
"$ref": "#/definitions/custom_filter"
}
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "The given team ID does not exist in the account"
}
}
},
"patch": {
"tags": [
"Custom Filter"
],
"operationId": "update-a-custom-filter",
"summary": "Update a custom filter",
"description": "Update a custom filter's attributes",
"parameters": [
{
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/custom_filter_create_update_payload"
}
}
],
"responses": {
"200": {
"description": "Success",
"schema": {
"$ref": "#/definitions/custom_filter"
}
},
"401": {
"description": "Unauthorized"
}
}
},
"delete": {
"tags": [
"Custom Filter"
],
"operationId": "delete-a-custom-filter",
"summary": "Delete a custom filter",
"description": "Delete a custom filter from the account",
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "The custom filter does not exist in the account"
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -2846,6 +3002,40 @@
} }
} }
}, },
"custom_filter": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The ID of the custom filter"
},
"name": {
"type": "string",
"description": "The name of the custom filter"
},
"type": {
"type": "string",
"enum": [
"conversation",
"contact",
"report"
],
"description": "The description about the custom filter"
},
"query": {
"type": "object",
"description": "A query that needs to be saved as a custom filter"
},
"created_at": {
"type": "datetime",
"description": "The time at which the custom filter was created"
},
"updated_at": {
"type": "datetime",
"description": "The time at which the custom filter was updated"
}
}
},
"account": { "account": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3191,6 +3381,28 @@
} }
} }
}, },
"custom_filter_create_update_payload": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the custom filter"
},
"type": {
"type": "string",
"enum": [
"conversation",
"contact",
"report"
],
"description": "The description about the custom filter"
},
"query": {
"type": "object",
"description": "A query that needs to be saved as a custom filter"
}
}
},
"integrations_hook_create_payload": { "integrations_hook_create_payload": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3643,6 +3855,15 @@
"required": true, "required": true,
"description": "The numeric ID of the user on the platform" "description": "The numeric ID of the user on the platform"
}, },
"custom_filter_id": {
"in": "path",
"name": "custom_filter_id",
"schema": {
"type": "integer"
},
"required": true,
"description": "The numeric ID of the custom filter"
},
"public_inbox_identifier": { "public_inbox_identifier": {
"in": "path", "in": "path",
"name": "inbox_identifier", "name": "inbox_identifier",
@ -3684,7 +3905,8 @@
"Messages", "Messages",
"Integrations", "Integrations",
"Profile", "Profile",
"Teams" "Teams",
"Custom Filter"
] ]
}, },
{ {