Merge branch 'decoupling-settings-page' of https://github.com/biosistemika/scinote-web into zd_SCI_1854

This commit is contained in:
zmagod 2018-01-09 10:03:02 +01:00
commit 0d7c116de1
18 changed files with 252 additions and 142 deletions

View file

@ -109,14 +109,14 @@
// Activity feed modal in main navigation menu
var activityModal = $('#activity-modal');
var activityModalBody = activityModal.find('.modal-body');
var initMoreBtn = function() {
activityModalBody.find('.btn-more-activities')
.on('ajax:success', function(e, data) {
$(data.html).insertBefore($(this).parents('li'));
$(this).attr('href', data.next_url);
if (data.activities_number < data.per_page) {
$(this).remove();
if(data.more_url) {
$(this).attr('href', data.more_url);
} else {
$(this).remove();
}
});
};

View file

@ -1,47 +1,43 @@
class ActivitiesController < ApplicationController
include ActivityHelper
before_action :load_vars
def index
@per_page = Constants::ACTIVITY_AND_NOTIF_SEARCH_LIMIT
@activities = current_user.last_activities(@last_activity_id,
@per_page + 1)
@overflown = @activities.length > @per_page
@activities = current_user.last_activities(@last_activity_id,
@per_page)
# Whether to hide date labels
@hide_today = params.include? :from
@day = @last_activity.present? ?
days_since_1970(@last_activity.created_at) :
days_since_1970(DateTime.current + 30.days)
more_url = url_for(activities_url(format: :json,
from: @activities.last.id)) if @overflown
respond_to do |format|
format.json {
render :json => {
per_page: @per_page,
activities_number: @activities.length,
next_url: more_url,
html: render_to_string({
partial: 'index.html.erb',
locals: {
more_activities_url: more_url,
hide_today: @hide_today,
day: @day
}
})
format.json do
render json: {
more_url: local_vars.fetch(:more_activities_url),
html: render_to_string(
partial: 'index.html.erb', locals: local_vars
)
}
}
end
end
end
def load_vars
@last_activity_id = params[:from].to_i || 0
@last_activity = Activity.find_by_id(@last_activity_id)
private
def local_vars
page = (params[:page] || 1).to_i
activities = current_user.last_activities
.page(page)
.per(Constants::ACTIVITY_AND_NOTIF_SEARCH_LIMIT)
unless activities.last_page?
more_url = url_for(
activities_url(
format: :json,
page: page + 1,
last_activity: activities.last.id
)
)
end
# send last activity date of the previus batch
previous_activity = Activity.find_by_id(params[:last_activity])
previus_date = previous_activity.created_at.to_date if previous_activity
{
activities: activities,
more_activities_url: more_url,
page: page,
previous_activity_created_at: previus_date
}
end
end

View file

@ -14,11 +14,17 @@ module ClientApi
private
def activities_vars
last_activity_id = params[:from].to_i || 0
per_page = Constants::ACTIVITY_AND_NOTIF_SEARCH_LIMIT
activities = current_user.last_activities(last_activity_id, per_page + 1)
more = activities.length > per_page
{ activities: activities, more: more }
page = (params[:page] || 1).to_i
activities = current_user
.last_activities
.page(page)
.per(Constants::ACTIVITY_AND_NOTIF_SEARCH_LIMIT)
{
activities: activities,
page: page,
more: !activities.last_page?,
timezone: current_user.time_zone
}
end
end
end

View file

@ -56,4 +56,15 @@ module ActivityHelper
def days_since_1970(dt)
dt.to_i / 86400
end
def calculate_previous_date(activities,
index,
previus_batch_last_activitiy_date)
if index == 1 && !activities.first_page?
return previus_batch_last_activitiy_date
end
activity = activities[index - 1]
return activity.created_at.to_date if activity
Date.new(1901, 1, 1)
end
end

View file

@ -1,10 +1,10 @@
// @flow
import React from "react";
import Moment from "react-moment";
import type { Node } from "react";
import { FormattedDate, FormattedMessage } from "react-intl";
import { Tooltip, OverlayTrigger } from "react-bootstrap";
import styled from "styled-components";
import { FormattedMessage } from "react-intl";
import {
WHITE_COLOR,
@ -14,8 +14,12 @@ import {
import { NAME_TRUNCATION_LENGTH } from "../../../config/constants/numeric";
type InputActivity = {
activity: Activity
}
const StyledLi = styled.li`
border-radius: .25em;
border-radius: 0.25em;
margin-bottom: 1em;
background-color: ${WHITE_COLOR};
border: 1px solid ${COLOR_CONCRETE};
@ -24,8 +28,8 @@ const TimeSpan = styled.span`
min-width: 150px;
display: table-cell;
vertical-align: middle;
border-top-left-radius: .25em;
border-bottom-left-radius: .25em;
border-top-left-radius: 0.25em;
border-bottom-left-radius: 0.25em;
border: 3px solid ${BORDER_GRAY_COLOR};
background-color: ${BORDER_GRAY_COLOR};
padding-left: 10px;
@ -37,52 +41,57 @@ const TextSpan = styled.span`
display: table-cell;
padding: 3px 10px;
text-align: justify;
`
function truncatedTooltip(id, text) {
`;
function truncatedTooltip(id: string, text: any): Node {
return (
<OverlayTrigger overlay={(
<Tooltip id={id}>
{text}
</Tooltip>
)} placement="bottom">
<span>
{text.substring(0, NAME_TRUNCATION_LENGTH)}...
</span>
<OverlayTrigger
overlay={<Tooltip id={id}>{text}</Tooltip>}
placement="bottom"
>
<span>{text.substring(0, NAME_TRUNCATION_LENGTH)}...</span>
</OverlayTrigger>
);
}
function taskPath(activity) {
function taskPath(activity: Activity): Node {
return (
<span>&nbsp;
[&nbsp;<FormattedMessage id="general.project" />:&nbsp;
<span>
&nbsp; [&nbsp;<FormattedMessage id="general.project" />:&nbsp;
{activity.project.length > NAME_TRUNCATION_LENGTH ? (
truncatedTooltip('activity_modal.long_project_tooltip', activity.project)
):(
truncatedTooltip(
"activity_modal.long_project_tooltip",
activity.project
)
) : (
<span>{activity.project}</span>
)},&nbsp;
<FormattedMessage id="general.task" />:&nbsp;
{activity.task.length > NAME_TRUNCATION_LENGTH ? (
truncatedTooltip('activity_modal.long_task_tooltip', activity.task)
):(
truncatedTooltip("activity_modal.long_task_tooltip", activity.task)
) : (
<span>{activity.task}</span>
)}&nbsp;]
</span>
);
}
const ActivityElement = ({ activity }) =>
const ActivityElement = ({ activity }: InputActivity ): Node => (
<StyledLi>
<TimeSpan>
<Moment format="HH.mm">
{activity.created_at}
</Moment>
<FormattedDate
value={new Date(activity.createdAt)}
hour="2-digit"
minute="2-digit"
timeZone={activity.timezone}
hour12={false}
/>
</TimeSpan>
<TextSpan>
<span dangerouslySetInnerHTML={{ __html: activity.message }} />
{activity.task && taskPath(activity)}
</TextSpan>
</StyledLi>;
</StyledLi>
);
export default ActivityElement;

View file

@ -1,7 +1,7 @@
// @flow
import React, { Component } from "react";
import type { Element } from "react";
import type { Element, Node } from "react";
import { FormattedMessage } from "react-intl";
import { Button, Modal } from "react-bootstrap";
import _ from "lodash";
@ -53,7 +53,8 @@ type Props = {
type State = {
activities: Array<Activity>,
more: boolean
more: boolean,
currentPage: number
};
class GlobalActivitiesModal extends Component<Props, State> {
@ -61,7 +62,7 @@ class GlobalActivitiesModal extends Component<Props, State> {
key: number,
activity: Activity,
date: Date
) {
): Node {
return [
<ActivityDateElement key={date} date={date} />,
<ActivityElement key={key} activity={activity} />
@ -70,7 +71,7 @@ class GlobalActivitiesModal extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { activities: [], more: false };
this.state = { activities: [], more: false, currentPage: 1 };
(this: any).displayActivities = this.displayActivities.bind(this);
(this: any).addMoreActivities = this.addMoreActivities.bind(this);
(this: any).onCloseModalActions = this.onCloseModalActions.bind(this);
@ -79,7 +80,7 @@ class GlobalActivitiesModal extends Component<Props, State> {
}
onCloseModalActions(): void {
this.setState({ activities: [], more: false });
this.setState({ activities: [], more: false, currentPage: 1 });
this.props.onCloseModal();
}
@ -87,37 +88,43 @@ class GlobalActivitiesModal extends Component<Props, State> {
getActivities().then(response => {
this.setState({
activities: response.activities,
more: response.more
more: response.more,
currentPage: response.currentPage
});
});
}
mapActivities(): Array<*> {
return this.state.activities.map((activity, i, arr) => {
// @todo replace key={i} with key={activity.id} !!!!!!!!!!!!!!
// when the backend bug will be fixed
const newDate = new Date(activity.created_at);
// returns a label with "today" if the date of the activity is today
if (i === 0) {
return GlobalActivitiesModal.renderActivityDateElement(
i,
activity,
newDate
);
return this.state.activities.map(
(activity: Activity, i: number, arr: Array<*>) => {
const newDate = new Date(activity.createdAt);
// returns a label with "today" if the date of the activity is today
if (i === 0 && newDate.toDateString() === new Date().toDateString()) {
return GlobalActivitiesModal.renderActivityDateElement(
activity.id,
activity,
newDate
);
}
// else checks if the previous activity is newer than current
// and displays a label with the date
const prevDate =
i !== 0 ? new Date(arr[i - 1].createdAt) : new Date(1901, 1, 1);
// filter only date from createdAt without minutes and seconds
// used to compare dates
const parsePrevDate = new Date(prevDate.toDateString());
const parseNewDate = new Date(newDate.toDateString());
if (parsePrevDate.getTime() > parseNewDate.getTime()) {
return GlobalActivitiesModal.renderActivityDateElement(
activity.id,
activity,
newDate
);
}
// returns the default activity element
return <ActivityElement key={activity.id} activity={activity} />;
}
// else checks if the previous activity is newer than current
// and displays a label with the date
const prevDate = new Date(arr[i - 1].created_at);
if (prevDate.getDate() > newDate.getDate()) {
return GlobalActivitiesModal.renderActivityDateElement(
i,
activity,
newDate
);
}
// returns the default activity element
return <ActivityElement key={i} activity={activity} />;
});
);
}
displayActivities() {
@ -136,15 +143,21 @@ class GlobalActivitiesModal extends Component<Props, State> {
}
addMoreActivities(): void {
const lastId = _.last(this.state.activities).id;
getActivities(
lastId
).then((response: { activities: Array<Activity>, more: boolean }) => {
this.setState({
activities: [...this.state.activities, ...response.activities],
more: response.more
});
});
this.state.currentPage + 1
).then(
(response: {
activities: Array<Activity>,
more: boolean,
currentPage: number
}) => {
this.setState({
activities: [...this.state.activities, ...response.activities],
more: response.more,
currentPage: response.currentPage
});
}
);
}
addMoreButton(): Element<*> {

View file

@ -14,9 +14,9 @@ import { getSciNoteInfo } from "../../../services/api/configurations_api";
import AboutScinoteModal from "./AboutScinoteModal";
type State = {
showModal: boolean,
scinoteVersion: string,
addons: Array<string>
addons: Array<string>,
showModal: boolean
};
class InfoDropdown extends Component<*, State> {

View file

@ -3,9 +3,9 @@ import axiosInstance from "./config";
import { ACTIVITIES_PATH } from "./endpoints";
export function getActivities(
lastId: number = 0
page: number = 1
): Promise<*> {
const path = `${ACTIVITIES_PATH}?from=${lastId}`;
const path = `${ACTIVITIES_PATH}?page=${page}`;
return axiosInstance.get(path).then(({ data }) => data.global_activities);
}

View file

@ -324,24 +324,24 @@ class User < ApplicationRecord
# Finds all activities of user that is assigned to project. If user
# is not an owner of the project, user must be also assigned to
# module.
def last_activities(last_activity_id = nil,
per_page = Constants::ACTIVITY_AND_NOTIF_SEARCH_LIMIT)
last_activity_id = Constants::INFINITY if last_activity_id < 1
def last_activities
Activity
.joins(project: :user_projects)
.joins("LEFT OUTER JOIN my_modules ON activities.my_module_id = my_modules.id")
.joins("LEFT OUTER JOIN user_my_modules ON my_modules.id = user_my_modules.my_module_id")
.where('activities.id < ?', last_activity_id)
.joins(
'LEFT OUTER JOIN my_modules ON activities.my_module_id = my_modules.id'
)
.joins(
'LEFT OUTER JOIN user_my_modules ON my_modules.id = ' \
'user_my_modules.my_module_id'
)
.where(user_projects: { user_id: self })
.where(
'activities.my_module_id IS NULL OR ' +
'user_projects.role = 0 OR ' +
'activities.my_module_id IS NULL OR ' \
'user_projects.role = 0 OR ' \
'user_my_modules.user_id = ?',
id
)
.order(created_at: :desc)
.limit(per_page)
.uniq
end
def self.find_by_valid_wopi_token(token)

View file

@ -1,12 +1,16 @@
<ul class="no-style double-line content-activities">
<% if @activities.length == 0 then %>
<% if activities.empty? %>
<li><em><%= t'activities.index.no_activities' %></em></li>
<% else %>
<%= render 'activities/list.html.erb', activities: @activities, hide_today: hide_today, day: @day %>
<%= render 'activities/list.html.erb',
activities: activities,
previous_activity_created_at: previous_activity_created_at %>
<% end %>
<% if @last_activity_id < 1 and @overflown %>
<% if more_activities_url.present? && page == 1 %>
<li class="text-center">
<a class="btn btn-default btn-more-activities" href="<%= more_activities_url %>" data-remote="true">
<a class="btn btn-default btn-more-activities"
href="<%= more_activities_url %>"
data-remote="true">
<%= t'activities.index.more_activities' %></a>
</li>
<% end %>

View file

@ -1,18 +1,13 @@
<%
current_day = days_since_1970(DateTime.current)
%>
<% if !hide_today && activities.count > 0 && days_since_1970(activities[0].created_at) == current_day %>
<% if activities.first_page? && activities.first.created_at.to_date == Date.today %>
<li class="text-center activity-date-item">
<span class="label label-primary">
<%=t "activities.index.today" %>
</span>
</li>
<% end %>
<% activities.each do |activity| %>
<% activity_day = days_since_1970(activity.created_at) %>
<% if activity_day < current_day and activity_day < day %>
<% day = days_since_1970(activity.created_at) %>
<% activities.each_with_index do |activity, index| %>
<% prevDate = calculate_previous_date(activities, index, previous_activity_created_at) %>
<% if activity.created_at.to_date < prevDate %>
<li class="text-center activity-date-item">
<span class="label label-primary">
<%= activity.created_at.strftime('%d.%m.%Y') %>

View file

@ -1,12 +1,14 @@
json.global_activities do
json.more more
json.currentPage page
json.activities activities do |activity|
json.id activity.id
json.message activity.message
json.createdAt activity.created_at
json.timezone timezone
if activity.my_module
json.project activity.my_module.experiment.project.name
json.task activity.my_module.name
end
json.created_at activity.created_at
end
end

7
flow-typed/types.js vendored
View file

@ -33,9 +33,10 @@ export type ValidationErrors = string | Array<string> | Array<ValidationError>;
export type Activity = {
id: number,
message: string,
created_at: string,
project?: string,
task?: string
createdAt: string,
timezone: string,
project: string,
task: string
};
export type Notification = {

View file

@ -0,0 +1,20 @@
require 'rails_helper'
describe ClientApi::ActivitiesController, type: :controller do
login_user
render_views
before do
project = create :project, created_by: User.first
UserProject.create(user: User.first, project: project, role: 2)
create :activity, user: User.first, project: project
end
describe '#index' do
it 'returns a valid object' do
get :index, format: :json
expect(response.status).to eq(200)
expect(response.body).to match_response_schema('activities')
end
end
end

View file

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :activity do
type_of :create_project
message Faker::Lorem.sentence(10)
project { Project.first || create(:project) }
end
end

View file

@ -0,0 +1,5 @@
FactoryGirl.define do
factory :user_project do
role 'owner'
end
end

View file

@ -179,4 +179,19 @@ describe User, type: :model do
it { is_expected.to respond_to(:recent_email_notification) }
it { is_expected.to respond_to(:system_message_email_notification) }
end
describe '#last_activities' do
let!(:user) { create :user }
let!(:project) { create :project }
let!(:user_projects) { create :user_project, project: project, user: user }
let!(:activity_one) { create :activity, user: user, project: project }
let!(:activity_two) { create :activity, user: user, project: project }
it 'is expected to return an array of user\'s activities' do
activities = user.last_activities
expect(activities.count).to eq 2
expect(activities).to include activity_one
expect(activities).to include activity_two
end
end
end

View file

@ -0,0 +1,26 @@
{
"type": "object",
"required": ["global_activities"],
"properties": {
"global_activities": {
"type": "object",
"required": ["activities", "more", "currentPage"],
"properties": {
"more": { "type": "boolean" },
"currentPage": { "type": "integer" },
"activities": {
"type": "array",
"items":{
"required": ["id", "message", "createdAt", "timezone"],
"properties": {
"id": { "type": "integer" },
"message": { "type": "string" },
"createdAt": { "type": "string" },
"timezone": { "type": "string" }
}
}
}
}
}
}
}