New Drive UI initial version
This commit is contained in:
@@ -4,8 +4,7 @@ import { Router } from 'react-router';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import MobileRedirect from './views/mobile-redirect';
|
||||
import Integration from 'app/views/integration';
|
||||
import MobileRedirect from './components/mobile-redirect';
|
||||
import RouterServices, { RouteType } from './features/router/services/router-service';
|
||||
import ErrorBoundary from 'app/views/error/error-boundary';
|
||||
import InitService from './features/global/services/init-service';
|
||||
@@ -56,41 +55,39 @@ export default () => {
|
||||
<RecoilRoot>
|
||||
<DebugState />
|
||||
<MobileRedirect>
|
||||
<Integration>
|
||||
<Router history={RouterServices.history}>
|
||||
<Switch>
|
||||
{RouterServices.routes.map((route: RouteType, index: number) => (
|
||||
<Route
|
||||
key={`${route.key}_${index}`}
|
||||
exact={route.exact ? route.exact : false}
|
||||
path={route.path}
|
||||
component={() =>
|
||||
route.options?.withErrorBoundary ? (
|
||||
<ErrorBoundary key={route.key}>
|
||||
<route.component />
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<route.component key={route.key} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
<Route
|
||||
path="/"
|
||||
component={() => {
|
||||
RouterServices.replace(
|
||||
`${
|
||||
RouterServices.pathnames.LOGIN
|
||||
}?auto&${RouterServices.history.location.search.substr(1)}`,
|
||||
);
|
||||
return <div />;
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Switch>
|
||||
</Router>
|
||||
</Integration>
|
||||
<Router history={RouterServices.history}>
|
||||
<Switch>
|
||||
{RouterServices.routes.map((route: RouteType, index: number) => (
|
||||
<Route
|
||||
key={`${route.key}_${index}`}
|
||||
exact={route.exact ? route.exact : false}
|
||||
path={route.path}
|
||||
component={() =>
|
||||
route.options?.withErrorBoundary ? (
|
||||
<ErrorBoundary key={route.key}>
|
||||
<route.component />
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<route.component key={route.key} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
<Route
|
||||
path="/"
|
||||
component={() => {
|
||||
RouterServices.replace(
|
||||
`${
|
||||
RouterServices.pathnames.LOGIN
|
||||
}?auto&${RouterServices.history.location.search.substr(1)}`,
|
||||
);
|
||||
return <div />;
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Switch>
|
||||
</Router>
|
||||
</MobileRedirect>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ interface AvatarProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
nogradient?: boolean;
|
||||
}
|
||||
|
||||
const sizes = { xl: 24, lg: 14, md: 11, sm: 9, xs: 6 };
|
||||
const sizes = { xl: 24, lg: 14, md: 10, sm: 9, xs: 6 };
|
||||
const fontSizes = { xl: '2xl', lg: '2xl', md: 'lg', sm: 'md', xs: 'sm' };
|
||||
|
||||
export const getGradient = (name: string) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
theme?: 'primary' | 'secondary' | 'danger' | 'default' | 'outline' | 'dark';
|
||||
theme?: 'primary' | 'secondary' | 'danger' | 'default' | 'outline' | 'dark' | 'white';
|
||||
size?: 'md' | 'lg' | 'sm';
|
||||
icon?: (props: any) => JSX.Element;
|
||||
iconSize?: 'md' | 'lg';
|
||||
@@ -19,18 +19,22 @@ export const Button = (props: ButtonProps) => {
|
||||
|
||||
if (props.theme === 'secondary')
|
||||
className =
|
||||
'text-blue-500 bg-blue-100 hover:bg-blue-200 active:bg-blue-300 border-transparent ';
|
||||
'text-blue-500 bg-blue-500 hover:bg-opacity-30 active:bg-opacity-50 bg-opacity-25 border-transparent ';
|
||||
|
||||
if (props.theme === 'danger')
|
||||
className = 'text-white bg-rose-500 hover:bg-rose-600 active:bg-rose-700 border-transparent ';
|
||||
|
||||
if (props.theme === 'default')
|
||||
className =
|
||||
'text-black dark:text-white bg-white dark:bg-zinc-800 dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-gray-50 active:bg-gray-200 border-gray-300';
|
||||
'text-black dark:text-white bg-white dark:bg-zinc-800 dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-zinc-50 active:bg-zinc-200 border-zinc-300';
|
||||
|
||||
if (props.theme === 'white')
|
||||
className =
|
||||
'text-black dark:text-white dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-zinc-50 active:bg-zinc-200 border-transparent';
|
||||
|
||||
if (props.theme === 'outline')
|
||||
className =
|
||||
'text-blue-500 bg-white dark:bg-zinc-800 dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-gray-50 active:bg-gray-200 border-blue-500';
|
||||
'text-blue-500 bg-white dark:bg-zinc-800 dark:hover:bg-zinc-700 dark:active:bg-zinc-900 hover:bg-zinc-50 active:bg-zinc-200 border-blue-500';
|
||||
|
||||
if (props.theme === 'dark')
|
||||
className =
|
||||
@@ -38,12 +42,12 @@ export const Button = (props: ButtonProps) => {
|
||||
|
||||
if (disabled) className += ' opacity-50 pointer-events-none';
|
||||
|
||||
if (props.size === 'lg') className = className + ' text-lg h-11';
|
||||
if (props.size === 'lg') className = className + ' text-lg h-10';
|
||||
else if (props.size === 'sm') className = className + ' text-sm h-7 px-3';
|
||||
else className = className + ' text-base h-9';
|
||||
|
||||
if (!props.children) {
|
||||
if (props.size === 'lg') className = className + ' w-11 !p-0 justify-center';
|
||||
if (props.size === 'lg') className = className + ' w-10 !p-0 justify-center';
|
||||
else if (props.size === 'sm') className = className + ' w-7 !p-0 justify-center';
|
||||
else className = className + ' w-9 !p-0 justify-center';
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ interface InputProps
|
||||
}
|
||||
|
||||
const baseInputClassName =
|
||||
'tw-input block w-full rounded-md focus:ring-1 focus:ring-blue-500 z-0 focus:z-10 dark:text-white text-black ';
|
||||
'tw-input block w-full rounded-md focus:ring-1 focus:ring-blue-500 z-0 focus:z-10 dark:text-white text-black text-base ';
|
||||
|
||||
export const defaultInputClassName = (theme: 'plain' | 'outline' = 'plain') => {
|
||||
return (
|
||||
baseInputClassName +
|
||||
(theme === 'plain'
|
||||
? 'bg-zinc-200 border-zinc-200 dark:bg-zinc-800 dark:border-zinc-800'
|
||||
? 'bg-zinc-100 border-zinc-100 dark:bg-zinc-800 dark:border-zinc-800'
|
||||
: 'bg-zinc-50 border-zinc-300 dark:bg-zinc-800 dark:border-zinc-700')
|
||||
);
|
||||
};
|
||||
@@ -47,7 +47,7 @@ export const Input = (props: InputProps) => {
|
||||
inputClassName = inputClassName + (props.disabled ? ' opacity-75' : '');
|
||||
|
||||
if (!props.multiline) {
|
||||
if (props.size === 'lg') inputClassName = inputClassName + ' h-11';
|
||||
if (props.size === 'lg') inputClassName = inputClassName + ' h-10 pl-10';
|
||||
else if (props.size === 'sm') inputClassName = inputClassName + ' h-7';
|
||||
else inputClassName = inputClassName + ' h-9';
|
||||
}
|
||||
|
||||
@@ -52,13 +52,13 @@ const Text = (props: TextProps) => {
|
||||
defaultClassName =
|
||||
'text-sm font-normal' +
|
||||
' ' +
|
||||
(props.noColor ? '' : 'text-zinc-400 dark:text-white dark:opacity-50');
|
||||
(props.noColor ? '' : 'text-zinc-500 dark:text-white dark:opacity-50');
|
||||
break;
|
||||
case 'info-small':
|
||||
defaultClassName =
|
||||
'text-xs font-normal' +
|
||||
' ' +
|
||||
(props.noColor ? '' : 'text-zinc-400 dark:text-white dark:opacity-50');
|
||||
(props.noColor ? '' : 'text-zinc-500 dark:text-white dark:opacity-50');
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import ConfiguratorsManager from 'app/deprecated/Configurators/ConfiguratorsManager.js';
|
||||
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
|
||||
import CloseIcon from '@material-ui/icons/CloseOutlined';
|
||||
import './configurators.scss';
|
||||
import Blocks from '../../views/applications/messages/message/parts/Blocks';
|
||||
|
||||
/*
|
||||
Where the configurators will be displayed, this component should be in app.js (menus should be over all elements of the page)
|
||||
*/
|
||||
export default class ConfigBodyLayer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {};
|
||||
ConfiguratorsManager.addListener(this);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ConfiguratorsManager.removeListener(this);
|
||||
clearTimeout(this.loading_interaction_timeout);
|
||||
}
|
||||
UNSAFE_componentWillMount() {
|
||||
this.generateData();
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
this.generateData();
|
||||
var string = JSON.stringify(this.configurator);
|
||||
if (string != this.saved) {
|
||||
nextState.loading_interaction = false;
|
||||
clearTimeout(this.loading_interaction_timeout);
|
||||
this.saved = string;
|
||||
return true;
|
||||
}
|
||||
return nextState.loading_interaction ? true : false;
|
||||
}
|
||||
generateData() {
|
||||
if (ConfiguratorsManager.configurator_order.length == 0) {
|
||||
this.configurator = null;
|
||||
}
|
||||
this.configurator =
|
||||
ConfiguratorsManager.currentConfigurators[
|
||||
ConfiguratorsManager.configurator_order[ConfiguratorsManager.configurator_order.length - 1]
|
||||
];
|
||||
}
|
||||
onAction(type, id, context, passives) {
|
||||
if (type == 'interactive_action') {
|
||||
this.setState({ loading_interaction: true });
|
||||
clearTimeout(this.loading_interaction_timeout);
|
||||
this.loading_interaction_timeout = setTimeout(() => {
|
||||
this.setState({ loading_interaction: false });
|
||||
}, 5000);
|
||||
var app_id = this.configurator.app.id;
|
||||
var ntype = 'interactive_configuration_action';
|
||||
var event = id;
|
||||
var data = {
|
||||
interactive_context: context,
|
||||
form: passives,
|
||||
hidden_data: this.configurator.hidden_data,
|
||||
configurator_id: this.configurator.id,
|
||||
};
|
||||
WorkspacesApps.notifyApp(app_id, ntype, event, data);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (!this.configurator) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
'config_modal fade_in ' + (this.state.loading_interaction ? 'loading_interaction ' : '')
|
||||
}
|
||||
>
|
||||
<div className="modal">
|
||||
<div
|
||||
className="component"
|
||||
style={{
|
||||
height: this.configurator.hidden_data.height || 'auto',
|
||||
width: this.configurator.hidden_data.width || '500px',
|
||||
maxWidth: '90vw',
|
||||
maxHeight: '90vh',
|
||||
}}
|
||||
>
|
||||
<div className="header">
|
||||
<div
|
||||
className="app_logo"
|
||||
style={{
|
||||
backgroundImage: 'url(' + this.configurator.app.identity?.icon + ')',
|
||||
}}
|
||||
/>
|
||||
<div className="app_name">{this.configurator.app.identity?.name}</div>
|
||||
|
||||
<CloseIcon
|
||||
className="m-icon-medium close"
|
||||
onClick={() => {
|
||||
ConfiguratorsManager.closeConfigurator(this.configurator.app);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="content">
|
||||
<Blocks
|
||||
className="allow_selection"
|
||||
blocks={this.configurator.form}
|
||||
fallback={''}
|
||||
onAction={(type, id, context, passives, evt) =>
|
||||
this.onAction(type, id, context, passives, evt)
|
||||
}
|
||||
allowAdvancedBlocks={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
.config_modal {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
position: absolute;
|
||||
z-index: 200;
|
||||
top: 0;
|
||||
|
||||
.modal {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
.component {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-base);
|
||||
box-shadow: var(--box-shadow-base);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
.header {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: flex;
|
||||
|
||||
.app_logo {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
border-radius: var(--border-radius-base);
|
||||
margin-right: 5px;
|
||||
}
|
||||
.app_name {
|
||||
line-height: 30px;
|
||||
flex: auto;
|
||||
}
|
||||
.close {
|
||||
margin: 2px;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import RouterServices from 'app/features/router/services/router-service';
|
||||
import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||
import workspaceUserRightsService from 'app/features/workspaces/services/workspace-user-rights-service';
|
||||
import Block from 'app/molecules/grouped-rows/base';
|
||||
import { addUrlTryDesktop } from 'app/views/desktop-redirect';
|
||||
import { addUrlTryDesktop } from 'app/components/desktop-redirect';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const ChannelSettingsMenu = (props: {
|
||||
|
||||
@@ -64,7 +64,7 @@ export default (): React.ReactElement => {
|
||||
style={{ maxHeight: 'calc(42vh - 98px)', minHeight: 'calc(42vh - 98px)' }}
|
||||
options={{ suppressScrollX: true, suppressScrollY: false }}
|
||||
>
|
||||
{ members_limit_reached && <ReachedLimit />}
|
||||
{members_limit_reached && <ReachedLimit />}
|
||||
<ChipInput
|
||||
value={emails}
|
||||
disableUnderline={true}
|
||||
|
||||
@@ -12,17 +12,17 @@ import { useCurrentUser } from 'app/features/users/hooks/use-current-user';
|
||||
const AccountStatusComponent = (): JSX.Element => {
|
||||
const { user } = useCurrentUser();
|
||||
const maxUnverifiedDays =
|
||||
InitService.server_infos?.configuration?.accounts?.console?.max_unverified_days || 7;
|
||||
InitService.server_infos?.configuration?.accounts?.remote?.max_unverified_days || 7;
|
||||
const oneDay = 1000 * 60 * 60 * 24;
|
||||
const periodLimit = (user?.created_at || 0) + maxUnverifiedDays * oneDay;
|
||||
const daysLeft = Math.ceil((periodLimit - Date.now()) / oneDay);
|
||||
|
||||
if (!user || InitService.server_infos?.configuration?.accounts?.type !== 'console') {
|
||||
if (!user || InitService.server_infos?.configuration?.accounts?.type !== 'remote') {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const showBlockedModal = () => {
|
||||
if (InitService.server_infos?.configuration?.accounts?.type === 'console')
|
||||
if (InitService.server_infos?.configuration?.accounts?.type === 'remote')
|
||||
return ModalManager.open(
|
||||
<BlockedAccount email={user?.email} />,
|
||||
{
|
||||
@@ -34,7 +34,7 @@ const AccountStatusComponent = (): JSX.Element => {
|
||||
};
|
||||
|
||||
const showUnverifiedModal = () => {
|
||||
if (InitService.server_infos?.configuration?.accounts?.type === 'console')
|
||||
if (InitService.server_infos?.configuration?.accounts?.type === 'remote')
|
||||
return ModalManager.open(
|
||||
<UnverifiedAccount daysLeft={daysLeft} limit={maxUnverifiedDays} email={user.email} />,
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ const CompanyStatusComponent = (): JSX.Element => {
|
||||
const workspace = DepreciatedCollections.get('workspaces').find(workspaceId);
|
||||
|
||||
useEffect(() => {
|
||||
if (InitService.server_infos?.configuration?.accounts?.type === 'console') {
|
||||
if (InitService.server_infos?.configuration?.accounts?.type === 'remote') {
|
||||
isNewAccount();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import useRouterCompany from 'app/features/router/hooks/use-router-company';
|
||||
import MessageQuote from 'app/molecules/message-quote';
|
||||
import { useUser } from 'app/features/users/hooks/use-user';
|
||||
import { useMessageQuoteReply } from 'app/features/messages/hooks/use-message-quote-reply';
|
||||
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
|
||||
import User from 'app/features/users/services/current-user-service';
|
||||
import { gotoMessage } from 'src/utils/messages';
|
||||
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
|
||||
import QuotedContent from 'app/molecules/quoted-content';
|
||||
import { NodeMessage } from 'app/features/messages/types/message';
|
||||
|
||||
type PropsType = {
|
||||
closable?: boolean;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export default ({ closable = true, onClose }: PropsType): React.ReactElement => {
|
||||
const companyId = useRouterCompany();
|
||||
const channelId = useRouterChannel();
|
||||
const workspaceId = useRouterWorkspace();
|
||||
const { message: quotedMessageId } = useMessageQuoteReply(channelId);
|
||||
const quotedMessage = useMessage({
|
||||
companyId,
|
||||
threadId: quotedMessageId,
|
||||
id: quotedMessageId,
|
||||
});
|
||||
|
||||
if (!quotedMessage.message) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const { message } = quotedMessage;
|
||||
const author = useUser(message.user_id);
|
||||
const authorName = author ? User.getFullName(author) : 'Anonymous';
|
||||
const deleted = message.subtype === 'deleted';
|
||||
|
||||
const quotedContent = (
|
||||
<QuotedContent message={message as unknown as NodeMessage['quote_message']} />
|
||||
);
|
||||
|
||||
return (
|
||||
<MessageQuote
|
||||
className="mx-1"
|
||||
message={quotedContent}
|
||||
author={authorName}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
deleted={deleted}
|
||||
goToMessage={() => gotoMessage(message, companyId, channelId, workspaceId)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -25,7 +25,7 @@ import { useRecoilValue, useRecoilState } from 'recoil';
|
||||
import { openDriveItem, onDriveItemDownloadClick } from '../common';
|
||||
import ResultContext from './result-context';
|
||||
import { useCompanyApplications } from 'app/features/applications/hooks/use-company-applications';
|
||||
import { DriveCurrentFolderAtom } from 'app/views/applications/drive/browser';
|
||||
import { DriveCurrentFolderAtom } from 'app/views/client/body/drive/browser';
|
||||
import { FolderIcon } from '@heroicons/react/solid';
|
||||
|
||||
export default (props: { driveItem: DriveItem & { user?: UserType } }) => {
|
||||
|
||||
@@ -49,6 +49,8 @@ export default class UploadZone extends React.Component<PropsType, StateType> {
|
||||
this.file_input.style.left = '-10000px';
|
||||
this.file_input.style.width = '100px';
|
||||
this.file_input.multiple = this.props.multiple ? true : false;
|
||||
this.file_input.directory = this.props.multiple ? true : false;
|
||||
this.file_input.webkitdirectory = this.props.multiple ? true : false;
|
||||
|
||||
this.setCallback();
|
||||
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
import React from 'react';
|
||||
import $ from 'jquery';
|
||||
import Observable from 'app/deprecated/CollectionsV1/observable.js';
|
||||
import popupManager from 'app/deprecated/popupManager/popupManager.js';
|
||||
import PopupManager from 'app/deprecated/popupManager/popupManager.js';
|
||||
import Api from 'app/features/global/framework/api-service';
|
||||
import ws from 'app/deprecated/websocket/websocket.js';
|
||||
import DepreciatedCollections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import Observable from 'app/deprecated/CollectionsV1/observable.js';
|
||||
import { default as popupManager, default as PopupManager } from 'app/deprecated/popupManager/popupManager.js';
|
||||
import ws from 'app/deprecated/websocket/websocket.js';
|
||||
import Groups from 'app/deprecated/workspaces/groups.js';
|
||||
import LocalStorage from 'app/features/global/framework/local-storage-service';
|
||||
import workspacesUsers from 'app/features/workspace-members/services/workspace-members-service';
|
||||
import WindowService from 'app/features/global/utils/window';
|
||||
import workspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
|
||||
import RouterServices from 'app/features/router/services/router-service';
|
||||
import NoWorkspaces from 'app/views/client/workspaces-bar/components/NoWorkspaces/NoWorkspaces';
|
||||
import NoCompanies from 'app/views/client/workspaces-bar/components/NoWorkspaces/NoCompanies';
|
||||
import loginService from 'app/features/auth/login-service';
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
import JWTStorage from 'app/features/auth/jwt-storage-service';
|
||||
import loginService from 'app/features/auth/login-service';
|
||||
import ConsoleService from 'app/features/console/services/console-service';
|
||||
import WorkspaceAPIClient from '../../features/workspaces/api/workspace-api-client';
|
||||
import Api from 'app/features/global/framework/api-service';
|
||||
import LocalStorage from 'app/features/global/framework/local-storage-service';
|
||||
import Logger from 'app/features/global/framework/logger-service';
|
||||
import UserAPIClient from 'app/features/users/api/user-api-client';
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
import WindowService from 'app/features/global/utils/window';
|
||||
import RouterServices from 'app/features/router/services/router-service';
|
||||
import workspacesUsers from 'app/features/workspace-members/services/workspace-members-service';
|
||||
import $ from 'jquery';
|
||||
import WorkspaceAPIClient from '../../features/workspaces/api/workspace-api-client';
|
||||
|
||||
class Workspaces extends Observable {
|
||||
constructor() {
|
||||
@@ -90,7 +85,6 @@ class Workspaces extends Observable {
|
||||
openNoWorkspacesPage() {
|
||||
this.showNoWorkspacesPage = true;
|
||||
this.notify();
|
||||
popupManager.open(<NoWorkspaces />, false, 'no_workspace_parameters');
|
||||
}
|
||||
|
||||
closeNoWorkspacesPage() {
|
||||
@@ -102,7 +96,6 @@ class Workspaces extends Observable {
|
||||
openNoCompaniesPage() {
|
||||
this.showNoCompaniesPage = true;
|
||||
this.notify();
|
||||
popupManager.open(<NoCompanies />, false, 'no_companies_parameters');
|
||||
}
|
||||
|
||||
closeNoCompaniesPage() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type EnvironmentType = {
|
||||
env_dev: boolean;
|
||||
env_dev_auth?: 'internal' | 'console';
|
||||
env_dev_auth?: 'internal' | 'remote';
|
||||
|
||||
api_root_url: string;
|
||||
front_root_url: string;
|
||||
|
||||
@@ -20,7 +20,7 @@ import Application from 'app/features/applications/services/application-service'
|
||||
import LocalStorage from 'app/features/global/framework/local-storage-service';
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
|
||||
type AccountType = 'console' | 'internal';
|
||||
type AccountType = 'remote' | 'internal';
|
||||
export type LoginState =
|
||||
| ''
|
||||
| 'app'
|
||||
@@ -60,7 +60,7 @@ class AuthService {
|
||||
|
||||
if (Globals.environment.env_dev_auth) accountType = Globals.environment.env_dev_auth;
|
||||
|
||||
if (accountType === 'console') {
|
||||
if (accountType === 'remote') {
|
||||
this.provider = new OIDCAuthProviderService(config as ConsoleConfiguration);
|
||||
} else if (accountType === 'internal') {
|
||||
this.provider = new InternalAuthProviderService(config as InternalConfiguration);
|
||||
|
||||
@@ -14,7 +14,6 @@ import LocalStorage from 'app/features/global/framework/local-storage-service';
|
||||
|
||||
const OIDC_CALLBACK_URL = '/oidccallback';
|
||||
const OIDC_SIGNOUT_URL = '/signout';
|
||||
const OIDC_CLIENT_ID = 'twake';
|
||||
|
||||
@TwakeService('OIDCAuthProvider')
|
||||
export default class OIDCAuthProviderService
|
||||
@@ -45,13 +44,15 @@ export default class OIDCAuthProviderService
|
||||
Oidc.Log.logger = Logger.getLogger('OIDCClient');
|
||||
Oidc.Log.level = EnvironmentService.isProduction() ? Oidc.Log.WARN : Oidc.Log.DEBUG;
|
||||
|
||||
const audience = ['https://dev-mxb6x0f2.eu.auth0.com/api/v2/'];
|
||||
|
||||
this.userManager = new Oidc.UserManager({
|
||||
userStore: new Oidc.WebStorageStateStore({ store: window.localStorage }),
|
||||
authority: this.configuration?.authority || environment.api_root_url,
|
||||
client_id: this.configuration?.client_id || OIDC_CLIENT_ID,
|
||||
client_id: this.configuration?.client_id,
|
||||
redirect_uri: getAsFrontUrl(OIDC_CALLBACK_URL),
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email address phone offline_access',
|
||||
scope: 'openid profile email address phone offline_access ' + audience.join(' '),
|
||||
post_logout_redirect_uri: getAsFrontUrl(OIDC_SIGNOUT_URL),
|
||||
//silent_redirect_uri: getAsFrontUrl(OIDC_SILENT_URL),
|
||||
automaticSilentRenew: true,
|
||||
@@ -236,7 +237,10 @@ export default class OIDCAuthProviderService
|
||||
this.logger.info('getJWTFromOidcToken, user expired');
|
||||
}
|
||||
|
||||
ConsoleService.getNewAccessToken({ access_token: user.access_token }, callback);
|
||||
ConsoleService.getNewAccessToken(
|
||||
{ id_token: user.id_token, access_token: user.access_token },
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
signinRedirect() {
|
||||
|
||||
@@ -109,7 +109,7 @@ class ConsoleService {
|
||||
* @param callback
|
||||
*/
|
||||
public getNewAccessToken(
|
||||
currentToken: { access_token: string },
|
||||
currentToken: { access_token: string; id_token: string },
|
||||
callback: (err?: Error, access_token?: JWTDataType) => void,
|
||||
): void {
|
||||
this.logger.debug(
|
||||
@@ -117,7 +117,7 @@ class ConsoleService {
|
||||
);
|
||||
Api.post(
|
||||
'/internal/services/console/v1/login',
|
||||
{ remote_access_token: currentToken.access_token },
|
||||
{ oidc_id_token: currentToken.id_token },
|
||||
(response: {
|
||||
access_token: JWTDataType;
|
||||
message: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ prefix.apply(log, {
|
||||
return level.toUpperCase();
|
||||
},
|
||||
nameFormatter(name) {
|
||||
return name || 'Twake';
|
||||
return name || 'Tdrive';
|
||||
},
|
||||
timestampFormatter(date) {
|
||||
return date.toISOString();
|
||||
|
||||
@@ -38,14 +38,15 @@ export type ServerInfoType = null | {
|
||||
help_url: string | null;
|
||||
pricing_plan_url: string | null;
|
||||
app_download_url: string | null;
|
||||
app_grid: { logo: string; name: string; url: string }[];
|
||||
mobile: {
|
||||
mobile_redirect: string;
|
||||
mobile_appstore: string;
|
||||
mobile_googleplay: string;
|
||||
};
|
||||
accounts: {
|
||||
type: 'console' | 'internal';
|
||||
console?: ConsoleConfiguration;
|
||||
type: 'remote' | 'internal';
|
||||
remote?: ConsoleConfiguration;
|
||||
internal?: InternalConfiguration;
|
||||
};
|
||||
};
|
||||
@@ -68,7 +69,7 @@ class InitService extends Observable {
|
||||
companyId = companyId || WorkspaceService.currentGroupId;
|
||||
const identity_provider_id =
|
||||
getCompany(companyId || '')?.identity_provider_id || getCompany(companyId || '')?.id;
|
||||
return (this.server_infos?.configuration?.accounts?.console?.[link] || '').replace(
|
||||
return (this.server_infos?.configuration?.accounts?.remote?.[link] || '').replace(
|
||||
/\{company_id\}/gm,
|
||||
identity_provider_id,
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { capitalize } from 'lodash';
|
||||
import Globals from 'app/features/global/services/globals-twake-app-service';
|
||||
|
||||
class WindowState {
|
||||
public readonly app_name: string = 'Twake';
|
||||
public readonly app_name: string = 'Tdrive';
|
||||
public prefix = '';
|
||||
public suffix = '';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import Login from 'app/views/login/login';
|
||||
import Logout from 'app/views/login/logout';
|
||||
import Error from 'app/views/error';
|
||||
import Join from 'app/views/join';
|
||||
import PublicMainView from 'app/views/applications/drive/shared';
|
||||
import PublicMainView from 'app/views/client/body/drive/shared';
|
||||
import Observable from '../../../deprecated/Observable/Observable';
|
||||
import { getWorkspacesByCompany } from 'app/features/workspaces/state/workspace-list';
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ export default function Block(props: BlockProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="grow truncate leading-normal text-slate-500 mr-1">
|
||||
<Text.Base className="text-slate-500 dark:text-slate-400">{props.subtitle}</Text.Base>
|
||||
<div className="grow truncate leading-normal text-zinc-500 mr-1">
|
||||
<Text.Base className="text-zinc-500 dark:text-zinc-400">{props.subtitle}</Text.Base>
|
||||
</div>
|
||||
<div className="whitespace-nowrap">{props.subtitle_suffix}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import {
|
||||
FileTypeArchiveIcon,
|
||||
FileTypeDocumentIcon,
|
||||
FileTypePdfIcon,
|
||||
FileTypeSlidesIcon,
|
||||
FileTypeSpreadsheetIcon,
|
||||
FileTypeUnknownIcon,
|
||||
} from 'app/atoms/icons-colored';
|
||||
import { Base } from 'app/atoms/text';
|
||||
import fileUploadApiClient from 'app/features/files/api/file-upload-api-client';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { useMessage, useSetMessage } from 'app/features/messages/hooks/use-message';
|
||||
import { NodeMessage } from 'app/features/messages/types/message';
|
||||
import { MessageContext } from 'app/views/applications/messages/message/message-with-replies';
|
||||
import MessageContent from 'app/views/applications/messages/message/parts/MessageContent';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
|
||||
type PropsType = {
|
||||
message: NodeMessage['quote_message'];
|
||||
};
|
||||
|
||||
export const useQuotedMessage = (
|
||||
message: NodeMessage,
|
||||
context: {
|
||||
companyId: string;
|
||||
workspaceId: string;
|
||||
channelId: string;
|
||||
threadId: string;
|
||||
id: string;
|
||||
},
|
||||
) => {
|
||||
const quotedMessageFromStore = useMessage({
|
||||
...context,
|
||||
threadId: message.quote_message?.thread_id as string,
|
||||
id: message.quote_message?.id as string,
|
||||
}).message;
|
||||
|
||||
const quotedMessage = {
|
||||
...message.quote_message,
|
||||
...(quotedMessageFromStore || message.quote_message),
|
||||
} as NodeMessage['quote_message'];
|
||||
|
||||
const setMessage = useSetMessage(quotedMessage?.company_id || context.companyId);
|
||||
useEffect(() => {
|
||||
if (!quotedMessageFromStore?.id && quotedMessage?.id) {
|
||||
setMessage(quotedMessage);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return quotedMessage;
|
||||
};
|
||||
|
||||
export default ({ message }: PropsType): React.ReactElement => {
|
||||
const context = useContext(MessageContext);
|
||||
|
||||
if (!message) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const attachmentType = fileUploadApiClient.mimeToType(message.files?.[0]?.metadata?.mime || '');
|
||||
|
||||
if (context.channelId && message.channel_id !== context.channelId) {
|
||||
return (
|
||||
<>
|
||||
<MessageContext.Provider
|
||||
value={{
|
||||
companyId: message.company_id,
|
||||
workspaceId: message.workspace_id,
|
||||
channelId: message.channel_id,
|
||||
threadId: message.thread_id,
|
||||
id: message.id || message.thread_id,
|
||||
}}
|
||||
>
|
||||
<MessageContent />
|
||||
</MessageContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{message.text && message.text.length ? (
|
||||
<Base className="!text-sm overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
{message.text}
|
||||
</Base>
|
||||
) : (
|
||||
<div className="mb-1"></div>
|
||||
)}
|
||||
{!!message.files?.length &&
|
||||
(message.files?.length === 1 ? (
|
||||
<div className="flex flex-row align-items items-center mb-1">
|
||||
<>
|
||||
{attachmentType === 'archive' ? (
|
||||
<FileTypeArchiveIcon className="w-5 h-5 mr-1" />
|
||||
) : attachmentType === 'pdf' ? (
|
||||
<FileTypePdfIcon className="w-5 h-5 mr-1" />
|
||||
) : attachmentType === 'document' ? (
|
||||
<FileTypeDocumentIcon className="w-5 h-5 mr-1" />
|
||||
) : attachmentType === 'spreadsheet' ? (
|
||||
<FileTypeSpreadsheetIcon className="w-5 h-5 mr-1" />
|
||||
) : attachmentType === 'slides' ? (
|
||||
<FileTypeSlidesIcon className={'h-5 w-5 mr-1'} />
|
||||
) : (
|
||||
<FileTypeUnknownIcon className="w-5 h-5 mr-1" />
|
||||
)}
|
||||
</>
|
||||
<Base className="!text-sm">{message.files?.[0].metadata?.name}</Base>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row align-items items-center mb-1">
|
||||
<FileTypeUnknownIcon className="w-5 h-5 mr-1" />
|
||||
<Base className="!text-sm">
|
||||
{Languages.t('molecules.quoted_content.attachements', [message.files?.length])}
|
||||
</Base>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -21,12 +21,14 @@ export default function Tab(props: TabsProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`overflow-auto flex w-100 border-b border-zinc-200 dark:border-zinc-800 transition-all select-none ${props.className}`}>
|
||||
<div
|
||||
className={`overflow-auto flex w-100 border-b border-zinc-200 dark:border-zinc-800 transition-all select-none ${props.className}`}
|
||||
>
|
||||
{props.tabs.map((tab, idx) => {
|
||||
const cl =
|
||||
defaultTabClassName +
|
||||
(idx === props.selected ? activeTabClassName : inactiveTabClassName)
|
||||
+ props.parentClassName;
|
||||
(idx === props.selected ? activeTabClassName : inactiveTabClassName) +
|
||||
props.parentClassName;
|
||||
return (
|
||||
<div key={idx} className={cl} onClick={() => props.onClick(idx)}>
|
||||
{tab}
|
||||
|
||||
@@ -6,7 +6,7 @@ html {
|
||||
color: var(--black);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: var(--secondary) !important;
|
||||
background-color: #FFF;
|
||||
}
|
||||
|
||||
#root {
|
||||
|
||||
@@ -1,850 +0,0 @@
|
||||
import React, { Component, useState } from 'react';
|
||||
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import UserService from 'app/features/users/services/current-user-service';
|
||||
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
|
||||
import LocalStorage from 'app/features/global/framework/local-storage-service';
|
||||
|
||||
import ModalManager from 'app/components/modal/modal-manager';
|
||||
|
||||
import Icon from 'app/components/icon/icon.jsx';
|
||||
import moment from 'moment';
|
||||
import Groups from 'app/deprecated/workspaces/groups.js';
|
||||
import FullCalendar from './full-calendar/full-calendar.jsx';
|
||||
import EventDetails from './modals/EventDetails.jsx';
|
||||
import EventCreation from './modals/EventCreation.jsx';
|
||||
import EventModification from './modals/EventModification.jsx';
|
||||
import CalendarEditor from './modals/CalendarEditor.jsx';
|
||||
import Menu from 'components/menus/menu.jsx';
|
||||
import DayPicker from 'components/calendar/day-picker/day-picker.jsx';
|
||||
import AlertManager from 'app/features/global/services/alert-manager-service';
|
||||
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
|
||||
import WorkspacesService from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
import popupManager from 'app/deprecated/popupManager/popupManager.js';
|
||||
import ConnectorsListManager from 'components/connectors-list-manager/connectors-list-manager.jsx';
|
||||
import WorkspaceUserRights from 'app/features/workspaces/services/workspace-user-rights-service';
|
||||
import Checkbox from 'app/components/inputs/deprecated_checkbox.jsx';
|
||||
import InputWithClipBoard from 'components/input-with-clip-board/input-with-clip-board.jsx';
|
||||
import Select from 'components/select/select.jsx';
|
||||
import WorkspaceParameter from 'app/views/client/popup/WorkspaceParameter/WorkspaceParameter.jsx';
|
||||
import UnconfiguredTab from './unconfigured-tab.jsx';
|
||||
import RouterService from 'app/features/router/services/router-service';
|
||||
import MainPlus from 'components/main-plus/main-plus.jsx';
|
||||
import {
|
||||
getCompanyApplication as getApplication,
|
||||
getCompanyApplications,
|
||||
} from 'app/features/applications/state/company-applications';
|
||||
|
||||
import './calendar.scss';
|
||||
|
||||
const ExportView = props => {
|
||||
const [export_my_calendar, set_export_my_calendar] = useState(props.values.export_my_calendar);
|
||||
const [export_workspace_calendar, set_export_workspace_calendar] = useState(
|
||||
props.values.export_workspace_calendar,
|
||||
);
|
||||
return (
|
||||
<div style={{ marginTop: -8 }}>
|
||||
<Checkbox
|
||||
label={Languages.t('scenes.apps.calendar.my_calendar_label', [], 'Mon calendrier')}
|
||||
small
|
||||
className=""
|
||||
value={export_my_calendar}
|
||||
onChange={value => {
|
||||
set_export_my_calendar(value);
|
||||
props.onChange(export_my_calendar, export_workspace_calendar);
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={Languages.t('scenes.apps.calendar.workspace_label', [], 'Cet espace de travail')}
|
||||
small
|
||||
className=""
|
||||
value={export_workspace_calendar}
|
||||
onChange={value => {
|
||||
set_export_workspace_calendar(value);
|
||||
props.onChange(export_my_calendar, export_workspace_calendar);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default class Calendar extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
i18n: Languages,
|
||||
view: 'timeGridWeek',
|
||||
filter: 'workspace',
|
||||
preview: null,
|
||||
export_my_calendar: true,
|
||||
export_workspace_calendar: true,
|
||||
new_calendar: {},
|
||||
};
|
||||
CalendarService.filter_mode = this.state.filter;
|
||||
|
||||
this.props = props;
|
||||
const { workspaceId, channelId } = RouterService.getStateFromRoute();
|
||||
|
||||
this.loaded_date_range = {};
|
||||
this.calendar_collection_key = 'calendar_' + workspaceId;
|
||||
|
||||
this.setLoadedRange(
|
||||
'both',
|
||||
moment(CalendarService.date).startOf('month').toDate().getTime() / 1000 - 60 * 60 * 24 * 31,
|
||||
moment(CalendarService.date).startOf('month').toDate().getTime() / 1000 +
|
||||
2 * 60 * 60 * 24 * 31,
|
||||
);
|
||||
|
||||
Collections.get('calendars').addListener(this);
|
||||
Collections.get('calendars').addSource(
|
||||
{
|
||||
http_base_url: 'calendar/calendar',
|
||||
http_options: {
|
||||
channel_id: channelId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
websockets: [
|
||||
{
|
||||
uri: 'calendars/' + workspaceId,
|
||||
options: { type: 'calendar' },
|
||||
},
|
||||
],
|
||||
},
|
||||
this.calendar_collection_key,
|
||||
() => {
|
||||
this.onFirstLoad();
|
||||
},
|
||||
);
|
||||
|
||||
if (Collections.get('calendars').did_load_first_time[this.calendar_collection_key]) {
|
||||
this.onFirstLoad();
|
||||
}
|
||||
|
||||
Collections.get('events').addListener(this);
|
||||
Languages.addListener(this);
|
||||
CalendarService.addListener(this);
|
||||
}
|
||||
onFirstLoad() {
|
||||
const { workspaceId, channelId } = RouterService.getStateFromRoute();
|
||||
var calendar_list = Collections.get('calendars')
|
||||
.findBy({ workspace_id: workspaceId })
|
||||
.map(cal => {
|
||||
return {
|
||||
calendar_id: cal.id,
|
||||
workspace_id: cal.workspace_id,
|
||||
};
|
||||
});
|
||||
if (this.props.tab != null && this.props.tab.configuration.calendars) {
|
||||
this.allowed_ids = this.props.tab.configuration.calendars.map(c => c.calendar_id);
|
||||
calendar_list = calendar_list.filter(c => this.allowed_ids.indexOf(c.calendar_id) >= 0);
|
||||
}
|
||||
Collections.get('events').addSource(
|
||||
{
|
||||
http_base_url: 'calendar/event',
|
||||
http_options: {
|
||||
channel_id: channelId,
|
||||
after_ts:
|
||||
this.loaded_date_range['both'].min || new Date().getTime() / 1000 - 24 * 60 * 60 * 60,
|
||||
before_ts:
|
||||
this.loaded_date_range['both'].max || new Date().getTime() / 1000 + 24 * 60 * 60 * 60,
|
||||
calendar_list: calendar_list,
|
||||
mode: 'both',
|
||||
},
|
||||
websockets: [
|
||||
{
|
||||
uri: 'calendar_events/' + workspaceId,
|
||||
options: { type: 'event' },
|
||||
},
|
||||
{
|
||||
uri: 'calendar_events/user/' + UserService.getCurrentUserId(),
|
||||
options: { type: 'event' },
|
||||
},
|
||||
],
|
||||
},
|
||||
this.calendar_collection_key,
|
||||
);
|
||||
}
|
||||
setLoadedRange(key, min, max) {
|
||||
if (!this.loaded_date_range[key]) {
|
||||
this.loaded_date_range[key] = {};
|
||||
}
|
||||
this.loaded_date_range[key].min = min;
|
||||
this.loaded_date_range[key].max = max;
|
||||
this.loaded_date_range[key].last_updated = new Date();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const view = LocalStorage.getItem('calendar_view');
|
||||
|
||||
if (view && this.calendar) {
|
||||
this.setState({ view });
|
||||
this.calendar.view(view);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Collections.get('events').removeListener(this);
|
||||
Collections.get('events').removeSource(this.calendar_collection_key);
|
||||
|
||||
Collections.get('calendars').removeListener(this);
|
||||
Collections.get('calendars').removeSource(this.calendar_collection_key);
|
||||
|
||||
if (CalendarService.preview) {
|
||||
CalendarService.closePopups();
|
||||
}
|
||||
|
||||
Languages.removeListener(this);
|
||||
CalendarService.removeListener(this);
|
||||
}
|
||||
UNSAFE_componentWillUpdate(nextProps, nextState) {
|
||||
if (nextProps.tab != null && nextProps.tab.configuration.calendars) {
|
||||
this.allowed_ids = nextProps.tab.configuration.calendars.map(c => c.calendar_id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var filter = nextState.filter;
|
||||
if (!this.loaded_date_range['both']) {
|
||||
this.loaded_date_range['both'] = {};
|
||||
}
|
||||
var range = this.loaded_date_range['both'];
|
||||
var requested_min =
|
||||
moment(CalendarService.date).startOf('month').toDate().getTime() / 1000 - 60 * 60 * 24 * 31;
|
||||
var requested_max =
|
||||
moment(CalendarService.date).startOf('month').toDate().getTime() / 1000 +
|
||||
2 * 60 * 60 * 24 * 31;
|
||||
if (
|
||||
!range.min ||
|
||||
range.min > requested_min ||
|
||||
range.max < requested_max ||
|
||||
new Date().getTime() - range.last_updated.getTime() > 60000
|
||||
) {
|
||||
this.setLoadedRange('both', requested_min, requested_max);
|
||||
|
||||
var calendar_list = Collections.get('calendars')
|
||||
.findBy({ workspace_id: RouterService.getStateFromRoute().workspaceId })
|
||||
.map(cal => {
|
||||
return {
|
||||
calendar_id: cal.id,
|
||||
workspace_id: cal.workspace_id,
|
||||
};
|
||||
});
|
||||
|
||||
if (this.props.tab != null) {
|
||||
calendar_list = calendar_list.filter(c => this.allowed_ids.indexOf(c.calendar_id) >= 0);
|
||||
}
|
||||
|
||||
Collections.get('events').sourceLoad(
|
||||
this.calendar_collection_key,
|
||||
{
|
||||
after_ts: requested_min,
|
||||
before_ts: requested_max,
|
||||
calendar_list: calendar_list,
|
||||
mode: 'both',
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
}
|
||||
componentDidUpdate() {
|
||||
if (this.date !== CalendarService.date && this.calendar) {
|
||||
this.calendar.setDate(CalendarService.date, true);
|
||||
this.date = CalendarService.date;
|
||||
}
|
||||
}
|
||||
configureCalendarConnector(app, cal) {
|
||||
var data = {
|
||||
calendar: cal,
|
||||
};
|
||||
WorkspacesApps.notifyApp(app.id, 'configuration', 'calendar', data);
|
||||
}
|
||||
renderCalendarList() {
|
||||
const { workspaceId } = RouterService.getStateFromRoute();
|
||||
var calendars = Collections.get('calendars').findBy({
|
||||
workspace_id: workspaceId,
|
||||
});
|
||||
if (this.props.tab != null) {
|
||||
calendars = calendars.filter(c => this.allowed_ids.indexOf(c.id) >= 0);
|
||||
}
|
||||
|
||||
if (calendars && calendars.length > 0) {
|
||||
var list = [];
|
||||
calendars.forEach(cal => {
|
||||
var el = (
|
||||
<div className="">
|
||||
<div className="calendar_color " style={{ backgroundColor: cal.color }} /> {cal.title}
|
||||
</div>
|
||||
);
|
||||
if (WorkspaceUserRights.hasWorkspacePrivilege()) {
|
||||
list.push({
|
||||
type: 'menu',
|
||||
text: el,
|
||||
submenu: [
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.modify_calendar_menu',
|
||||
[],
|
||||
'Modifier le calendrier',
|
||||
),
|
||||
submenu_replace: true,
|
||||
submenu: [
|
||||
{
|
||||
type: 'title',
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.edit_calendar_title',
|
||||
[],
|
||||
'Éditer le calendrier',
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'react-element',
|
||||
reactElement: level => (
|
||||
<CalendarEditor
|
||||
calendar={Collections.get('calendars').edit(cal)}
|
||||
level={level}
|
||||
collectionKey={this.calendar_collection_key}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
className: 'error',
|
||||
text: Languages.t('general.delete', [], 'Supprimer'),
|
||||
onClick: () => {
|
||||
AlertManager.confirm(
|
||||
() => {
|
||||
Collections.get('calendars').remove(cal, this.calendar_collection_key);
|
||||
},
|
||||
() => {},
|
||||
{
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.remove_calendar_confirmation',
|
||||
[],
|
||||
'Supprimer le calendrier et ses événements définitivement ?',
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t('scenes.apps.calendar.connectors_menu', [], 'Connecteurs...'),
|
||||
submenu: [
|
||||
{
|
||||
type: 'react-element',
|
||||
reactElement: level => {
|
||||
var apps = getCompanyApplications(Groups.currentGroupId).filter(app => false);
|
||||
if (apps.length > 0) {
|
||||
return (
|
||||
<ConnectorsListManager
|
||||
list={apps}
|
||||
current={(cal.connectors || [])
|
||||
.map(id => getApplication(id))
|
||||
.filter(item => item)}
|
||||
configurable={item =>
|
||||
((item.display || {}).configuration || {}).can_configure_in_calendar
|
||||
}
|
||||
onChange={ids => {
|
||||
cal.connectors = ids;
|
||||
Collections.get('calendars').save(cal, this.calendar_collection_key);
|
||||
}}
|
||||
onConfig={app => {
|
||||
this.configureCalendarConnector(app, cal);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="menu-text" style={{ margin: 0, padding: 0 }}>
|
||||
{Languages.t(
|
||||
'scenes.apps.calendar.no_connectors_menu_text',
|
||||
[],
|
||||
"Vous n'avez aucun connecteur capable de se connecter à un calendrier.",
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.connectors_search_menu',
|
||||
[],
|
||||
'Chercher des connecteurs...',
|
||||
),
|
||||
onClick: () => {
|
||||
popupManager.open(
|
||||
<WorkspaceParameter initial_page={3} options={'open_search_apps'} />,
|
||||
true,
|
||||
'workspace_parameters',
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
list.push({ type: 'menu', text: el });
|
||||
}
|
||||
});
|
||||
return list;
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.no_calendar_text',
|
||||
[],
|
||||
"Vous n'avez défini aucun calendrier pour cet espace de travail.",
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
export(download) {
|
||||
if (!this.state.export_my_calendar && !this.state.export_workspace_calendar) {
|
||||
return;
|
||||
}
|
||||
var mode = 'mine';
|
||||
var calendar_list = [];
|
||||
if (this.state.export_my_calendar && this.state.export_workspace_calendar) {
|
||||
mode = 'both';
|
||||
} else if (this.state.export_workspace_calendar) {
|
||||
mode = 'workspace';
|
||||
}
|
||||
|
||||
if (mode === 'both' || mode === 'workspace') {
|
||||
calendar_list = Collections.get('calendars')
|
||||
.findBy({ workspace_id: RouterService.getStateFromRoute().workspaceId })
|
||||
.map(cal => {
|
||||
return {
|
||||
calendar_id: cal.id,
|
||||
workspace_id: cal.workspace_id,
|
||||
};
|
||||
});
|
||||
if (this.props.tab != null) {
|
||||
calendar_list = calendar_list.filter(c => this.allowed_ids.indexOf(c.calendar_id) >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
CalendarService.export(
|
||||
WorkspacesService.currentWorkspaceId,
|
||||
{ mode: mode, calendar_list: calendar_list },
|
||||
download,
|
||||
token => {
|
||||
if (!download) {
|
||||
AlertManager.alert(() => {}, { text: <InputWithClipBoard value={token} /> });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
completeRect(rect) {
|
||||
rect.x = rect.x || rect.left;
|
||||
rect.y = rect.y || rect.top;
|
||||
return rect;
|
||||
}
|
||||
render() {
|
||||
const { workspaceId } = RouterService.getStateFromRoute();
|
||||
if (
|
||||
this.props.tab != null &&
|
||||
(!this.props.tab.configuration || this.props.tab.configuration.calendars === undefined)
|
||||
) {
|
||||
return (
|
||||
<UnconfiguredTab
|
||||
saveTab={this.props.saveTab}
|
||||
channel={this.props.channel}
|
||||
tab={this.props.tab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
var calendars = Collections.get('calendars')
|
||||
.findBy({ workspace_id: workspaceId })
|
||||
.map(cal => cal.id);
|
||||
|
||||
if (this.props.tab != null) {
|
||||
calendars = calendars.filter(c => this.allowed_ids.indexOf(c) >= 0);
|
||||
}
|
||||
|
||||
var events = Collections.get('events')
|
||||
.findBy({})
|
||||
.filter(event => {
|
||||
if (!event.id) {
|
||||
return true;
|
||||
}
|
||||
event._user_transparent = false;
|
||||
|
||||
var not_mine =
|
||||
(event.participants || []).filter(
|
||||
part => part.user_id_or_mail === UserService.getCurrentUserId(),
|
||||
).length === 0;
|
||||
if (this.state.filter === 'mine') {
|
||||
if (not_mine) {
|
||||
event._user_transparent = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (this.state.filter === 'workspace' || this.state.filter === 'custom') {
|
||||
//Not in this workspace
|
||||
if (
|
||||
event.workspaces_calendars.filter(part => calendars.indexOf(part.calendar_id) >= 0)
|
||||
.length === 0
|
||||
) {
|
||||
if (!not_mine) {
|
||||
//Set transparent event
|
||||
event._user_transparent = true;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (
|
||||
CalendarService.edited &&
|
||||
CalendarService.edited.from &&
|
||||
CalendarService.edited.from !== this.lastEditedFrom
|
||||
) {
|
||||
this.lastEditedFrom = CalendarService.edited.from;
|
||||
this.calendar.setDate(new Date(CalendarService.edited.from * 1000), true);
|
||||
|
||||
if (!CalendarService.edited.id) {
|
||||
setTimeout(() => {
|
||||
var htmlEl = this.calendar.getDomElement(CalendarService.edited);
|
||||
if (htmlEl) {
|
||||
ModalManager.updateHighlight(this.completeRect(window.getBoundingClientRect(htmlEl)));
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
var calendar_menu = [
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t('scenes.apps.calendar.my_calendar_menu', [], 'Mon calendrier'),
|
||||
onClick: () => {
|
||||
this.setState({ filter: 'mine' });
|
||||
CalendarService.filter_mode = 'mine';
|
||||
Menu.closeAll();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t('scenes.apps.calendar.workspace_menu', [], 'Espace de travail'),
|
||||
onClick: () => {
|
||||
this.setState({ filter: 'workspace' });
|
||||
CalendarService.filter_mode = 'workspace';
|
||||
Menu.closeAll();
|
||||
},
|
||||
},
|
||||
/*{type:"menu", text:"Personnaliser...", onClick: ()=>{
|
||||
|
||||
}},*/
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t('scenes.apps.calendar.export_view_menu', [], 'Exporter la vue'),
|
||||
submenu: [
|
||||
{ type: 'title', text: Languages.t('scenes.apps.calendar.export_title', [], 'Exporter') },
|
||||
{
|
||||
type: 'react-element',
|
||||
reactElement: () => {
|
||||
return (
|
||||
<ExportView
|
||||
values={this.state}
|
||||
onChange={(my, ws) => {
|
||||
this.setState({
|
||||
export_my_calendar: my,
|
||||
export_workspace_calendar: ws,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.ics_subscription_menu',
|
||||
[],
|
||||
"Obtenir un lien d'abonnement ICS",
|
||||
),
|
||||
onClick: () => {
|
||||
this.export(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.ics_download_menu',
|
||||
[],
|
||||
'Télécharger un fichier ICS',
|
||||
),
|
||||
onClick: () => {
|
||||
this.export(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
];
|
||||
|
||||
calendar_menu = calendar_menu.concat(this.renderCalendarList());
|
||||
|
||||
if (this.props.tab === null && WorkspaceUserRights.hasWorkspacePrivilege()) {
|
||||
calendar_menu = calendar_menu.concat([
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.add_calendar_menu',
|
||||
[],
|
||||
'Ajouter un calendrier...',
|
||||
),
|
||||
submenu_replace: true,
|
||||
submenu: [
|
||||
{
|
||||
type: 'title',
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.create_calendar_title',
|
||||
[],
|
||||
'Créer un calendrier',
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'react-element',
|
||||
reactElement: level => {
|
||||
return (
|
||||
<CalendarEditor level={level} collectionKey={this.calendar_collection_key} />
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
var list = [];
|
||||
|
||||
list.push(
|
||||
<div className="calendar_app">
|
||||
<div className="calendar_header">
|
||||
<div className="left">
|
||||
<Menu
|
||||
className="select medium"
|
||||
style={{ width: 'auto' }}
|
||||
position="bottom"
|
||||
menu={calendar_menu}
|
||||
>
|
||||
{
|
||||
{
|
||||
mine: Languages.t('scenes.apps.calendar.my_calendar', [], 'Mon calendrier'),
|
||||
workspace: Languages.t('scenes.apps.calendar.workspace', [], 'Espace de travail'),
|
||||
}[this.state.filter]
|
||||
}
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
<div className="right">
|
||||
{this.state.view !== 'dayGridMonth' && (
|
||||
<div className="week_number">
|
||||
{Languages.t('scenes.apps.calendar.calendar.week_btn', [], 'Semaine')}{' '}
|
||||
{moment(CalendarService.date).week()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Menu
|
||||
className={'current_date ' + moment(CalendarService.date).format('DD-MM-YYYY')}
|
||||
position="bottom"
|
||||
menu={[
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t('scenes.apps.calendar.today_menu', [], "Aujourd'hui"),
|
||||
onClick: () => this.calendar.today(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'react-element',
|
||||
reactElement: () => (
|
||||
<div style={{ padding: '4px 8px' }}>
|
||||
<DayPicker
|
||||
value={moment(CalendarService.date)}
|
||||
onChange={value => {
|
||||
this.calendar.setDate(value);
|
||||
Menu.closeAll();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{CalendarService.date &&
|
||||
(this.state.view === 'dayGridMonth' ||
|
||||
this.state.view === 'timeGridWeek' ||
|
||||
this.state.view === 'listYear') &&
|
||||
moment(CalendarService.date).format('MMMM YYYY')}
|
||||
{CalendarService.date &&
|
||||
this.state.view === 'timeGridDay' &&
|
||||
moment(CalendarService.date).format('LL')}
|
||||
</Menu>
|
||||
|
||||
<div className="move">
|
||||
<Icon
|
||||
className="m-icon-small left"
|
||||
type="arrow-left"
|
||||
onClick={() => this.calendar.previous()}
|
||||
/>
|
||||
<Icon
|
||||
className="m-icon-small right"
|
||||
type="arrow-right"
|
||||
onClick={() => this.calendar.next()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="view_selector">
|
||||
<Select
|
||||
medium
|
||||
style={{ width: 'auto' }}
|
||||
value={this.state.view}
|
||||
onChange={value => {
|
||||
this.calendar.view(value);
|
||||
LocalStorage.setItem('calendar_view', value);
|
||||
Menu.closeAll();
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
value: 'dayGridMonth',
|
||||
text: Languages.t('scenes.apps.calendar.month_option', [], 'Mois'),
|
||||
},
|
||||
{
|
||||
value: 'timeGridWeek',
|
||||
text: Languages.t('scenes.apps.calendar.week_option', [], 'Semaine'),
|
||||
},
|
||||
{
|
||||
value: 'timeGridDay',
|
||||
text: Languages.t('scenes.apps.calendar.day_option', [], 'Jour'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FullCalendar
|
||||
i18n={Languages.language}
|
||||
ref={node => (this.calendar = node)}
|
||||
onViewChange={view => this.setState({ view: view })}
|
||||
onDateChange={date => {
|
||||
CalendarService.date = date;
|
||||
CalendarService.notify();
|
||||
}}
|
||||
date={CalendarService.date}
|
||||
onCreate={_event => {
|
||||
CalendarService.edit(_event);
|
||||
CalendarService.fullSizeModal = false;
|
||||
|
||||
setTimeout(() => {
|
||||
var htmlEl = this.calendar.getDomElement(CalendarService.edited);
|
||||
ModalManager.open(
|
||||
<EventCreation
|
||||
event={CalendarService.edited}
|
||||
collectionKey={this.calendar_collection_key}
|
||||
/>,
|
||||
{
|
||||
highlight: htmlEl
|
||||
? this.completeRect(window.getBoundingClientRect(htmlEl))
|
||||
: null,
|
||||
position: 'left',
|
||||
margin: 5,
|
||||
no_background: true,
|
||||
size: { width: 440 },
|
||||
},
|
||||
);
|
||||
}, 100);
|
||||
}}
|
||||
onUpdate={(event, htmlEl) => {
|
||||
//Collections.get("events").updateObject(event);
|
||||
if (event.id) {
|
||||
CalendarService.save(event, this.calendar_collection_key);
|
||||
}
|
||||
if (
|
||||
!CalendarService.fullSizeModal &&
|
||||
CalendarService.preview &&
|
||||
CalendarService.preview.front_id === event.front_id
|
||||
) {
|
||||
var updated = CalendarService.preview;
|
||||
setTimeout(() => {
|
||||
var e = this.calendar.getDomElement(updated);
|
||||
e &&
|
||||
ModalManager.updateHighlight(this.completeRect(window.getBoundingClientRect(e)));
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
onClickOut={() => {
|
||||
CalendarService.closePopups();
|
||||
}}
|
||||
onClickEvent={(event, jsEvent) => {
|
||||
jsEvent.stopPropagation();
|
||||
jsEvent.preventDefault();
|
||||
if (
|
||||
!ModalManager.isOpen() ||
|
||||
!CalendarService.preview ||
|
||||
CalendarService.preview.front_id !== event.front_id
|
||||
) {
|
||||
CalendarService.fullSizeModal = false;
|
||||
CalendarService.startPreview(event);
|
||||
setTimeout(() => {
|
||||
var htmlEl = this.calendar.getDomElement(CalendarService.preview);
|
||||
ModalManager.open(
|
||||
<EventDetails event={event} collectionKey={this.calendar_collection_key} />,
|
||||
{
|
||||
highlight: htmlEl
|
||||
? this.completeRect(window.getBoundingClientRect(htmlEl))
|
||||
: null,
|
||||
position: 'left',
|
||||
margin: 5,
|
||||
no_background: true,
|
||||
size: { width: 440 },
|
||||
},
|
||||
);
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
events={events}
|
||||
getCalendar={id => Collections.get('calendars').find(id)}
|
||||
/>
|
||||
|
||||
<MainPlus
|
||||
onClick={() => {
|
||||
CalendarService.fullSizeModal = true;
|
||||
CalendarService.edit({
|
||||
from: new Date().getTime() / 1000,
|
||||
to: new Date().getTime() / 1000 + 60 * 60,
|
||||
});
|
||||
setTimeout(() => {
|
||||
ModalManager.open(
|
||||
<EventModification
|
||||
event={CalendarService.edited}
|
||||
collectionKey={this.calendar_collection_key}
|
||||
/>,
|
||||
{ size: { width: 600 } },
|
||||
);
|
||||
}, 100);
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
.calendar_app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.menu {
|
||||
.calendar_color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar_header {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right {
|
||||
& > div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.current_date {
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.move {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
|
||||
.m-icon-small {
|
||||
font-size: 24px !important;
|
||||
vertical-align: middle;
|
||||
margin-top: -4px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.week_number {
|
||||
color: var(--white);
|
||||
background: var(--grey-dark);
|
||||
border-radius: var(--border-radius-base);
|
||||
padding: 1px 4px;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unconfigured_tab {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
margin-top: 10vh;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from 'react';
|
||||
import { ViewConfiguration } from '../../../features/router/services/app-view-service';
|
||||
import { useTab } from '../../../features/tabs/hooks/use-tabs';
|
||||
import CalendarContent from './calendar-content';
|
||||
|
||||
type Props = {
|
||||
options: ViewConfiguration;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const tabId = props.options?.context?.tabId;
|
||||
const { tab, save } = useTab(tabId);
|
||||
|
||||
return (
|
||||
<CalendarContent
|
||||
options={props.options}
|
||||
tab={tab}
|
||||
saveTab={(configuration: any) => {
|
||||
save({ ...tab, configuration });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export default class Event extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
import '@fullcalendar/core';
|
||||
import '@fullcalendar/core/main.css';
|
||||
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import momentPlugin from '@fullcalendar/moment';
|
||||
import momentTimezonePlugin from '@fullcalendar/moment-timezone';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import listPlugin from '@fullcalendar/list';
|
||||
import FullCalendarPlugin from '@fullcalendar/react';
|
||||
|
||||
import '@fullcalendar/daygrid/main.css';
|
||||
import '@fullcalendar/timegrid/main.css';
|
||||
import '@fullcalendar/list/main.css';
|
||||
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
|
||||
import EventUI from 'components/calendar/event/event.jsx';
|
||||
import WorkspaceService from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import './full-calendar.scss';
|
||||
|
||||
export default class FullCalendar extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.calendarRef = null;
|
||||
window.react_calendar = this;
|
||||
this.event_dom_elements = {};
|
||||
this.eventsById = {};
|
||||
|
||||
this.clickOut = this.clickOut.bind(this);
|
||||
}
|
||||
|
||||
getDomElement(event) {
|
||||
return this.event_dom_elements[event.front_id];
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
var that = this;
|
||||
var scrollTime = moment().format('HH') + ':00:00';
|
||||
this.options = {
|
||||
defaultDate: this.props.date || new Date(),
|
||||
header: false,
|
||||
timezone: 'local',
|
||||
height: 'parent',
|
||||
locale: this.props.i18n,
|
||||
nowIndicator: true,
|
||||
scrollTime: scrollTime,
|
||||
firstHour: new Date().getUTCHours() - 5,
|
||||
allDaySlot: true,
|
||||
editable: true,
|
||||
selectable: true,
|
||||
selectHelper: true,
|
||||
firstDay: moment().startOf('week').isoWeekday(),
|
||||
slotDuration: '00:30:00',
|
||||
snapDuration: '00:15:00',
|
||||
columnHeaderHtml: mom => {
|
||||
return ReactDOMServer.renderToString(
|
||||
<div className="">
|
||||
<div className="number">{moment(mom).format('D')}</div>
|
||||
<div className="day">{moment(mom).format('ddd')}</div>
|
||||
</div>,
|
||||
);
|
||||
},
|
||||
|
||||
eventAllow: function (dropLocation, draggedEvent) {
|
||||
// if(!that.state.calendar.calendars[draggedEvent.calendar]){
|
||||
// return false;
|
||||
// }
|
||||
return true;
|
||||
},
|
||||
select: function (event) {
|
||||
that.cancelClickOut = true;
|
||||
|
||||
//Create event
|
||||
if (moment(event.end).diff(event.start) === 15 * 60 * 1000) {
|
||||
that.api.unselect();
|
||||
that.props.onClickOut && that.props.onClickOut();
|
||||
return false;
|
||||
}
|
||||
if (that.props.onCreate) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var created_event = that.props.onCreate(
|
||||
that.fcToCollection(event),
|
||||
that.event_dom_elements[event.id],
|
||||
);
|
||||
}
|
||||
if (that.props.onUpdate) {
|
||||
that.props.onUpdate(that.fcToCollection(event), that.event_dom_elements[event.id]);
|
||||
}
|
||||
},
|
||||
eventResize: function (event) {
|
||||
event = event.event;
|
||||
if (that.props.onUpdate) {
|
||||
that.props.onUpdate(that.fcToCollection(event), that.event_dom_elements[event.id]);
|
||||
}
|
||||
},
|
||||
eventDrop: function (event) {
|
||||
event = event.event;
|
||||
if (that.props.onUpdate) {
|
||||
that.props.onUpdate(that.fcToCollection(event), that.event_dom_elements[event.id]);
|
||||
}
|
||||
},
|
||||
eventClick: function (_event) {
|
||||
if (this.disableUpdate) {
|
||||
this.disableUpdate = false;
|
||||
this.refreshEvent();
|
||||
}
|
||||
|
||||
var event = _event.event;
|
||||
|
||||
if (that.props.onClickEvent) {
|
||||
that.props.onClickEvent(that.fcToCollection(event), _event.jsEvent, _event.el);
|
||||
}
|
||||
|
||||
//if(!event.private_content){ViewsServiceImpl.open();that.state.calendar.selectEvent(event);}
|
||||
},
|
||||
eventRender: function (event) {
|
||||
if (event.event && event.event.id && event.isStart && !event.isMirror) {
|
||||
that.event_dom_elements[event.event.id] = event.el;
|
||||
}
|
||||
var col_event = that.eventsById[event.event.id];
|
||||
|
||||
if (col_event._user_transparent) {
|
||||
event.el.classList.add('transparent');
|
||||
} else {
|
||||
event.el.classList.remove('transparent');
|
||||
}
|
||||
|
||||
var color = '';
|
||||
if (col_event.workspaces_calendars && col_event.workspaces_calendars[0]) {
|
||||
var calendar = null;
|
||||
(col_event.workspaces_calendars || []).some(cal => {
|
||||
var calendar_id = cal.calendar_id;
|
||||
if (calendar_id && that.props.getCalendar) {
|
||||
var tmp = that.props.getCalendar(calendar_id);
|
||||
if (tmp && tmp.workspace_id === WorkspaceService.currentWorkspaceId) {
|
||||
calendar = tmp;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
color = calendar ? calendar.color : '#92929C';
|
||||
}
|
||||
|
||||
if (color) {
|
||||
event.el.style.backgroundColor = color;
|
||||
}
|
||||
|
||||
if (col_event.type === 'deadline' || col_event.type === 'remind') {
|
||||
event.el.classList.add('not_resizable');
|
||||
}
|
||||
|
||||
event.el.firstChild.innerHTML = ReactDOMServer.renderToString(
|
||||
<EventUI event={col_event} getColor={cal_id => color || 'var(--primary)'} />,
|
||||
);
|
||||
},
|
||||
eventResizeStart: () => {
|
||||
that.cancelClickOut = true;
|
||||
this.disableUpdate = true;
|
||||
},
|
||||
eventDragStart: () => {
|
||||
that.cancelClickOut = true;
|
||||
this.disableUpdate = true;
|
||||
},
|
||||
eventResizeStop: () => {
|
||||
that.cancelClickOut = false;
|
||||
this.disableUpdate = false;
|
||||
if (this.missedRefresh) {
|
||||
this.refreshEvent();
|
||||
}
|
||||
},
|
||||
eventDragStop: () => {
|
||||
that.cancelClickOut = false;
|
||||
this.disableUpdate = false;
|
||||
if (this.missedRefresh) {
|
||||
this.refreshEvent();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
var page = window.document.getElementsByClassName('appPage')[0];
|
||||
if (page) {
|
||||
page.removeEventListener('click', this.clickOut);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
var page = window.document.getElementsByClassName('appPage')[0];
|
||||
if (page) {
|
||||
page.addEventListener('click', this.clickOut);
|
||||
}
|
||||
|
||||
if (this.calendarRef) {
|
||||
this.api = this.calendarRef.getApi();
|
||||
}
|
||||
this.refreshEvent();
|
||||
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.refreshEvent();
|
||||
}
|
||||
|
||||
clickOut() {
|
||||
if (this.cancelClickOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onClickOut && this.props.onClickOut();
|
||||
}
|
||||
|
||||
refreshEvent() {
|
||||
if (this.disableUpdate) {
|
||||
this.missedRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.missedRefresh = false;
|
||||
|
||||
var events = this.props.events.map(event => this.collectionToFc(event));
|
||||
//Monkey fix for events not appearing randomly
|
||||
this.api.removeAllEvents();
|
||||
this.api.addEventSource(events);
|
||||
this.api.rerenderEvents();
|
||||
}
|
||||
|
||||
previous() {
|
||||
this.willChangeView(false);
|
||||
this.api.prev();
|
||||
this.props.onDateChange && this.props.onDateChange(this.api.getDate());
|
||||
}
|
||||
|
||||
next() {
|
||||
this.willChangeView(true);
|
||||
this.api.next();
|
||||
this.props.onDateChange && this.props.onDateChange(this.api.getDate());
|
||||
}
|
||||
|
||||
today() {
|
||||
this.willChangeView();
|
||||
this.api.today();
|
||||
this.props.onDateChange && this.props.onDateChange(this.api.getDate());
|
||||
}
|
||||
|
||||
setDate(date, disableAnimation) {
|
||||
if (!disableAnimation) {
|
||||
this.willChangeView();
|
||||
}
|
||||
this.api.gotoDate(moment(date).toDate());
|
||||
this.props.onDateChange && this.props.onDateChange(moment(date).toDate());
|
||||
}
|
||||
|
||||
view(view) {
|
||||
if (view !== this.api.type) {
|
||||
this.willChangeView();
|
||||
this.api.changeView(view);
|
||||
this.props.onViewChange && this.props.onViewChange(view);
|
||||
this.props.onDateChange && this.props.onDateChange(this.api.getDate());
|
||||
}
|
||||
}
|
||||
|
||||
willChangeView(forward) {
|
||||
var old_effect =
|
||||
this.calendarRef.elRef.current.parentElement.getElementsByClassName('false_calendar')[0];
|
||||
if (old_effect) {
|
||||
this.calendarRef.elRef.current.parentElement.removeChild(old_effect);
|
||||
}
|
||||
var calendar = this.calendarRef.elRef.current;
|
||||
calendar.classList = 'fc fc-ltr fc-unthemed';
|
||||
var scroll_state = calendar.getElementsByClassName('fc-scroller')[0];
|
||||
if (scroll_state) {
|
||||
scroll_state = scroll_state.scrollTop;
|
||||
} else {
|
||||
scroll_state = 0;
|
||||
}
|
||||
|
||||
var clone = calendar.cloneNode(true);
|
||||
|
||||
calendar.classList.add('calendar_invisible');
|
||||
setTimeout(() => {
|
||||
this.calendarRef.elRef.current.classList.remove('calendar_invisible');
|
||||
if (forward === undefined) {
|
||||
this.calendarRef.elRef.current.classList.add('calendar_appear');
|
||||
} else if (forward) {
|
||||
this.calendarRef.elRef.current.classList.add('calendar_appear_right');
|
||||
} else {
|
||||
this.calendarRef.elRef.current.classList.add('calendar_appear_left');
|
||||
}
|
||||
}, 20);
|
||||
|
||||
clone.classList.add('calendar_disappear');
|
||||
clone.classList.add('false_calendar');
|
||||
calendar.parentElement.append(clone);
|
||||
var scroller = clone.getElementsByClassName('fc-scroller')[0];
|
||||
if (scroll_state !== scroller.scrollTop) {
|
||||
scroller.scrollTop = scroll_state;
|
||||
}
|
||||
}
|
||||
|
||||
fcToCollection(event) {
|
||||
return {
|
||||
id: event.extendedProps ? event.extendedProps.real_id : '',
|
||||
front_id: event.id,
|
||||
from:
|
||||
(event.allDay
|
||||
? moment(moment(event.start).format('YYYY-MM-DDT00:00:00+00:00')).valueOf()
|
||||
: moment(event.start).utc().valueOf()) / 1000,
|
||||
to:
|
||||
(event.allDay
|
||||
? moment(moment(event.end).format('YYYY-MM-DDT00:00:00+00:00')).valueOf()
|
||||
: moment(event.end).utc().valueOf()) /
|
||||
1000 -
|
||||
(event.allDay ? 24 * 60 * 60 : 0),
|
||||
all_day: event.allDay,
|
||||
//repetition_definition: { string: event.rrule, duration: event.duration },
|
||||
};
|
||||
}
|
||||
|
||||
collectionToFc(event) {
|
||||
var force_allday = false;
|
||||
if (Math.abs(event.from - event.to) > 60 * 60 * 24 * 2) {
|
||||
force_allday = true;
|
||||
}
|
||||
|
||||
this.eventsById[event.front_id] = event;
|
||||
|
||||
var from = event.from;
|
||||
var to = event.to;
|
||||
|
||||
if (event.type === 'remind' || event.type === 'deadline') {
|
||||
to = parseInt(from) + 15 * 60;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
var event = {
|
||||
id: event.front_id,
|
||||
real_id: event.id,
|
||||
start: moment(from * 1000).toDate(),
|
||||
end: moment(to * 1000 + (event.all_day ? 24 * 60 * 60 * 1000 : 0)).toDate(),
|
||||
allDay: event.all_day || force_allday,
|
||||
title: event.title || Languages.t('scenes.apps.drive.navigators.new_file.untitled'),
|
||||
//rrule: (event.repetition_definition || {}).string, //'DTSTART:20190201T103000Z\nRRULE:FREQ=WEEKLY;INTERVAL=5;UNTIL=20190601;BYDAY=MO,FR',
|
||||
duration: (event.repetition_definition || {}).duration,
|
||||
editable: !CalendarService.getIsReadonly(event),
|
||||
};
|
||||
return event;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="twake_fullcalendar show_day_line">
|
||||
<FullCalendarPlugin
|
||||
defaultView="timeGridWeek"
|
||||
ref={node => (this.calendarRef = node)}
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
momentPlugin,
|
||||
momentTimezonePlugin,
|
||||
interactionPlugin,
|
||||
listPlugin,
|
||||
]}
|
||||
{...this.options}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
.twake_fullcalendar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
||||
& > div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.false_calendar {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Annimations */
|
||||
.calendar_invisible {
|
||||
opacity: 0;
|
||||
}
|
||||
.calendar_disappear {
|
||||
animation: calendar_disappear 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
.calendar_disappear_left {
|
||||
animation: calendar_disappear_left 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
.calendar_disappear_right {
|
||||
animation: calendar_disappear_right 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
.calendar_appear {
|
||||
animation: calendar_disappear 0.2s reverse;
|
||||
opacity: 1;
|
||||
}
|
||||
.calendar_appear_left {
|
||||
animation: calendar_disappear_left 0.2s reverse;
|
||||
opacity: 1;
|
||||
}
|
||||
.calendar_appear_right {
|
||||
animation: calendar_disappear_right 0.2s reverse;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes calendar_disappear {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes calendar_disappear_left {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes calendar_disappear_right {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(50px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fc.fc-unthemed {
|
||||
}
|
||||
|
||||
.fc-divider.fc-widget-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fc-widget-content {
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
&.show_day_line {
|
||||
.fc-timeGridWeek-view {
|
||||
.fc-day-grid {
|
||||
min-height: 2.5em;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fc-timeGridWeek-view {
|
||||
.fc-day-grid {
|
||||
transition: height 0.2s;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-dayGridMonth-view {
|
||||
.fc-week:last-child {
|
||||
.fc-day {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-day-grid .fc-bg table {
|
||||
border: unset;
|
||||
border-bottom: none;
|
||||
|
||||
.fc-day {
|
||||
border-top: 0px;
|
||||
border-right: 0px;
|
||||
}
|
||||
|
||||
.fc-today {
|
||||
background: var(--white) !important;
|
||||
}
|
||||
|
||||
.fc-axis.fc-widget-content {
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-scroller {
|
||||
border-top: 1px solid var(--grey-background);
|
||||
}
|
||||
|
||||
.fc-head .fc-head-container {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fc-day-header,
|
||||
.fc-widget-header.fc-axis {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-slats .fc-minor td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.fc-timeGrid-view .fc-day-grid .fc-row {
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-slats td {
|
||||
border-top: none;
|
||||
height: 2.5em;
|
||||
span {
|
||||
margin-top: -18px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-now-indicator.fc-now-indicator-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-now-indicator-line {
|
||||
border-top-width: 2px;
|
||||
border-color: var(--primary);
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--primary);
|
||||
position: absolute;
|
||||
left: -4px;
|
||||
top: -5px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
tr .fc-day-header .number {
|
||||
margin-top: 0px;
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
tr .fc-day-header .day {
|
||||
padding-bottom: 10px;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
th.fc-today.fc-day-header {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.fc-month-view {
|
||||
tr .fc-day-header .number {
|
||||
display: none;
|
||||
height: 0px !important;
|
||||
}
|
||||
.fc-day,
|
||||
.fc-unthemed td.fc-today {
|
||||
border-top: 1px solid var(--grey-background) !important;
|
||||
}
|
||||
.fc-day-top {
|
||||
background: transparent !important;
|
||||
}
|
||||
.fc-day-top .fc-day-number {
|
||||
float: left;
|
||||
margin: 5px;
|
||||
}
|
||||
.fc-day-header .day {
|
||||
padding-bottom: 0;
|
||||
padding-left: 6px;
|
||||
padding-top: 6px;
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.fc-unthemed td.fc-today {
|
||||
background: #fff;
|
||||
border-right: 0;
|
||||
border-top: 1px;
|
||||
border-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.fc td {
|
||||
border-left: 1px solid var(--grey-background);
|
||||
}
|
||||
|
||||
.fc td:first-child {
|
||||
border-left: 0px;
|
||||
font-size: 12px;
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.fc-unthemed th,
|
||||
.fc-unthemed td,
|
||||
.fc-unthemed thead,
|
||||
.fc-unthemed tbody,
|
||||
.fc-unthemed .fc-divider,
|
||||
.fc-unthemed .fc-row,
|
||||
.fc-unthemed .fc-content,
|
||||
.fc-unthemed .fc-popover,
|
||||
.fc-unthemed .fc-list-view,
|
||||
.fc-unthemed .fc-list-heading td {
|
||||
border-color: var(--grey-background);
|
||||
color: var(--grey-dark);
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
.fc-widget-content {
|
||||
border-bottom: 1px solid var(--grey-background);
|
||||
}
|
||||
.fc-time {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-day-top.fc-today {
|
||||
color: var(--primary);
|
||||
background: var(--grey-background);
|
||||
}
|
||||
.fc-unthemed td.fc-today {
|
||||
background: var(--grey-background);
|
||||
border-right: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.fc-axis {
|
||||
width: 40px !important;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.fc-time-grid-event .fc-time span {
|
||||
color: var(--white);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fc-event {
|
||||
min-height: 18px;
|
||||
background-color: var(--primary);
|
||||
border: 0;
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: visible !important;
|
||||
transition: z-index 0.5s;
|
||||
transition-delay: 0s;
|
||||
border-radius: var(--border-radius-base);
|
||||
margin-bottom: 0;
|
||||
background: #ffffff00 !important;
|
||||
display: flex;
|
||||
|
||||
&.not_resizable {
|
||||
.fc-resizer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
box-shadow: 0 0 4px 1px #549ae6;
|
||||
border-bottom: 0px !important;
|
||||
}
|
||||
|
||||
&:not(.selected):hover {
|
||||
transition-delay: 0.5s;
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
.fc-content {
|
||||
background: transparent;
|
||||
transition: max-height 0.5s, background 0.5s, transform 0.5s, border-radius 0.5s,
|
||||
box-shadow 0.5s, min-width 0.5s;
|
||||
min-height: calc(100% - 10px);
|
||||
min-width: 100%;
|
||||
transition-delay: 0s;
|
||||
border-radius: 0px;
|
||||
color: var(--white);
|
||||
|
||||
.event_container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fc-title {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
|
||||
.fc-time {
|
||||
opacity: 0.9;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-title,
|
||||
.fc-time,
|
||||
.fc-time span {
|
||||
transition: color 0.5s;
|
||||
transition-delay: 0s;
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.fc-bg {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.fc-time {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&.event-deadline {
|
||||
max-height: 23px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
left: 0% !important;
|
||||
right: 0% !important;
|
||||
padding-top: 3px;
|
||||
border-top: 3px solid #ff000a !important;
|
||||
.fc-resizer {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.event-move {
|
||||
border-radius: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
&.event-reminder {
|
||||
max-height: 23px;
|
||||
border-radius: 16px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
min-height: 23px;
|
||||
.fc-resizer {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.private_content {
|
||||
background-color: var(--black) !important;
|
||||
border-radius: 0 !important;
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAO0lEQVQImWNMS0tjgIF79+7B2UxYRZWUlJiwikJ1YIoyMDAwCwoKYoqi2IEseu/ePSasolAdmKIMDAwAGk0Wdkl75tAAAAAASUVORK5CYII=') !important;
|
||||
background-repeat: repeat;
|
||||
cursor: default;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
opacity: 0.3 !important;
|
||||
|
||||
& > div {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fc-ltr .fc-time-grid .fc-event-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fc-content .fc-title {
|
||||
font-size: 14px;
|
||||
color: var(--white);
|
||||
|
||||
.icon.event-type {
|
||||
margin-right: 5px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.participant {
|
||||
display: inline-block;
|
||||
margin: 0 2px 2px 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 50%;
|
||||
background-color: var(--white);
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon:before {
|
||||
top: 30px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-day-grid-event .fc-time {
|
||||
font-weight: normal;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.fc-list-heading td {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.fc-list-item-marker {
|
||||
width: 10px;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.fc-list-item-title {
|
||||
border-left: none !important;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.fc-list-item-title > a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-slats td {
|
||||
height: 2em;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/* eslint-disable react/no-direct-mutation-state */
|
||||
import React, { Component } from 'react';
|
||||
import InputWithColor from 'components/inputs/input-with-color.jsx';
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import WorkspaceService from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
import Button from 'components/buttons/button.jsx';
|
||||
import Menu from 'components/menus/menu.jsx';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
|
||||
export default class CalendarEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
calendar: Collections.get('calendars').editCopy(props.calendar || {}),
|
||||
};
|
||||
}
|
||||
save() {
|
||||
this.state.calendar.workspace_id = WorkspaceService.currentWorkspaceId;
|
||||
Collections.get('calendars').save(this.state.calendar, this.props.collectionKey);
|
||||
Menu.closeAll();
|
||||
}
|
||||
setKey(obj) {
|
||||
Object.keys(obj || {}).forEach(k => {
|
||||
this.state.calendar[k] = obj[k];
|
||||
});
|
||||
this.setState({});
|
||||
}
|
||||
render() {
|
||||
var calendar = this.state.calendar;
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<InputWithColor
|
||||
className="medium bottom-margin full_width"
|
||||
focusOnDidMount
|
||||
menu_level={this.props.level}
|
||||
placeholder={Languages.t('scenes.apps.calendar.calendar_modal.placeholder', [], 'Name')}
|
||||
value={[this.state.calendar.color, this.state.calendar.title]}
|
||||
onEnter={() => this.save()}
|
||||
onChange={value => {
|
||||
this.setKey({ color: value[0], title: value[1] });
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="menu-buttons">
|
||||
<Button
|
||||
disabled={(this.state.calendar.title || '').length <= 0}
|
||||
type="button"
|
||||
value={
|
||||
this.state.calendar.id
|
||||
? Languages.t('general.save', [], 'Enregistrer')
|
||||
: Languages.t('general.add', [], 'Ajouter')
|
||||
}
|
||||
onClick={() => this.save()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import MediumPopupManager from 'app/components/modal/modal-manager';
|
||||
import EventModification from './EventModification.jsx';
|
||||
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
|
||||
import Input from 'components/inputs/input.jsx';
|
||||
import InputIcon from 'components/inputs/input-icon.jsx';
|
||||
import Participants from './Part/Participants.jsx';
|
||||
import DateSelector from './Part/DateSelector.jsx';
|
||||
import CalendarSelector from 'components/calendar/calendar-selector/calendar-selector.jsx';
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import Button from 'components/buttons/button.jsx';
|
||||
import Select from 'components/select/select.jsx';
|
||||
import WorkspaceService from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
import Icon from 'app/components/icon/icon.jsx';
|
||||
|
||||
export default class EventCreation extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
Collections.get('events').addListener(this);
|
||||
Collections.get('events').listenOnly([props.event.front_id], this);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
CalendarService.fullSizeModal = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MediumPopupManager.mountedComponent = this;
|
||||
this.doNotCancelEdit = false;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
Collections.get('events').removeListener(this);
|
||||
}
|
||||
|
||||
onMediumPopupClose() {
|
||||
if (!this.doNotCancelEdit) {
|
||||
CalendarService.closePopups();
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
CalendarService.saveEdit(this.props.collectionKey);
|
||||
}
|
||||
change(key, value, notify) {
|
||||
this.props.event[key] = value;
|
||||
|
||||
if (notify || notify === undefined) {
|
||||
this.update_timeout && clearTimeout(this.update_timeout);
|
||||
this.update_timeout = setTimeout(() => {
|
||||
Collections.get('events').notify();
|
||||
}, 1000);
|
||||
this.setState({});
|
||||
}
|
||||
}
|
||||
render() {
|
||||
var event = this.props.event;
|
||||
var calendar_list = Collections.get('calendars').findBy({
|
||||
workspace_id: WorkspaceService.currentWorkspaceId,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="eventModal event_creation" style={{ padding: '16px' }}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={event.title || ''}
|
||||
placeholder={Languages.t('scenes.apps.calendar.modals.title_placeholder', [], 'Titre')}
|
||||
onChange={evt => {
|
||||
this.change('title', evt.target.value);
|
||||
}}
|
||||
className="full_width bottom-margin"
|
||||
big
|
||||
/>
|
||||
|
||||
<div className="bottom-margin date_and_type">
|
||||
<Select
|
||||
medium
|
||||
value={event.type || 'event'}
|
||||
onChange={value => {
|
||||
this.change('type', value);
|
||||
}}
|
||||
options={CalendarService.event_types}
|
||||
className="right-margin"
|
||||
/>
|
||||
<DateSelector
|
||||
event={event}
|
||||
onChange={(from, to, all_day, repetition_definition) => {
|
||||
this.change('from', from, false);
|
||||
this.change('to', to, false);
|
||||
this.change('all_day', all_day, false);
|
||||
this.change('repetition_definition', repetition_definition);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex' }} className="full_width bottom-margin">
|
||||
<div style={{ flex: 1, display: 'flex' }} className="right-margin">
|
||||
<InputIcon
|
||||
icon={(event.location || '').slice(0, 4) == 'http' ? 'link' : 'location-point'}
|
||||
medium
|
||||
value={event.location || ''}
|
||||
placeholder={Languages.t(
|
||||
'scenes.apps.calendar.modals.event_adresse_placeholder',
|
||||
[],
|
||||
'Adresse',
|
||||
)}
|
||||
onChange={evt => {
|
||||
this.change('location', evt.target.value);
|
||||
}}
|
||||
className="full_width"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="button medium secondary"
|
||||
onClick={() => {
|
||||
this.change(
|
||||
'location',
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
'/bundle/connectors/jitsi/call/twake_event_' +
|
||||
(event.front_id || '').replace(/-/g, '_') +
|
||||
'__' +
|
||||
(event.front_id || '').replace(/-/g, '_'),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon type="video" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<InputIcon
|
||||
autoHeight
|
||||
icon="align-left-justify"
|
||||
medium
|
||||
value={event.description || ''}
|
||||
placeholder={Languages.t(
|
||||
'scenes.apps.calendar.modals.description_placeholder',
|
||||
[],
|
||||
'Description',
|
||||
)}
|
||||
onChange={evt => {
|
||||
this.change('description', evt.target.value);
|
||||
}}
|
||||
className="full_width bottom-margin"
|
||||
/>
|
||||
|
||||
<CalendarSelector
|
||||
medium
|
||||
value={event.workspaces_calendars || []}
|
||||
onChange={workspaces_calendars => {
|
||||
this.change('workspaces_calendars', workspaces_calendars);
|
||||
}}
|
||||
calendarList={calendar_list || []}
|
||||
className=""
|
||||
/>
|
||||
|
||||
<div className="separator" />
|
||||
|
||||
<div className="small-bottom-margin">
|
||||
<Participants
|
||||
participants={event.participants}
|
||||
owner={event.owner}
|
||||
onChange={user_id_or_mail => this.change('participants', user_id_or_mail)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="separator" />
|
||||
|
||||
<Button
|
||||
className="button medium secondary-light"
|
||||
style={{ width: 'auto' }}
|
||||
onClick={() => {
|
||||
this.doNotCancelEdit = true;
|
||||
MediumPopupManager.open(
|
||||
<EventModification
|
||||
event={CalendarService.edited}
|
||||
collectionKey={this.props.collectionKey}
|
||||
/>,
|
||||
{ size: { width: 600 } },
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Languages.t('scenes.apps.calendar.modals.advanced_options', [], 'Options avancées')}
|
||||
</Button>
|
||||
<Button
|
||||
className="button medium btn-primary medium"
|
||||
style={{ width: 'auto', marginLeft: 10, float: 'right' }}
|
||||
onClick={() => {
|
||||
this.save();
|
||||
}}
|
||||
>
|
||||
{Languages.t('general.save', [], 'Enregistrer')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import moment from 'moment';
|
||||
import MediumPopupManager from 'app/components/modal/modal-manager';
|
||||
import EventModification from './EventModification.jsx';
|
||||
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
|
||||
import Participants from './Part/Participants.jsx';
|
||||
import CalendarSelector from 'components/calendar/calendar-selector/calendar-selector.jsx';
|
||||
import AlertManager from 'app/features/global/services/alert-manager-service';
|
||||
import DateTimeUtils from 'app/features/global/utils/datetime.js';
|
||||
import Button from 'components/buttons/button.jsx';
|
||||
import Icon from 'app/components/icon/icon.jsx';
|
||||
import Tabs from 'components/tabs/tabs.jsx';
|
||||
import SearchService from 'app/deprecated/search/search.js';
|
||||
import WorkspacesService from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
import UserService from 'app/features/users/services/current-user-service';
|
||||
|
||||
export default class EventDetails extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
Collections.get('events').addListener(this);
|
||||
if (props.event.front_id) {
|
||||
Collections.get('events').listenOnly([props.event.front_id], this);
|
||||
}
|
||||
}
|
||||
UNSAFE_componentWillMount() {
|
||||
CalendarService.fullSizeModal = false;
|
||||
}
|
||||
componentDidMount() {
|
||||
MediumPopupManager.mountedComponent = this;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
Collections.get('events').removeListener(this);
|
||||
}
|
||||
remove() {
|
||||
var event = Collections.get('events').findByFrontId(this.props.event.front_id);
|
||||
AlertManager.confirm(
|
||||
() => {
|
||||
CalendarService.remove(event, this.props.collectionKey);
|
||||
},
|
||||
() => {},
|
||||
{
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.modals.remove_event_text',
|
||||
[],
|
||||
Languages.t(
|
||||
'scenes.apps.calendar.modals.remove_event_alert_confirmation',
|
||||
[],
|
||||
"Supprimer l'événement ?",
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
render() {
|
||||
var event = Collections.get('events').findByFrontId(this.props.event.front_id);
|
||||
|
||||
if (!event) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var from = new Date(event.from * 1000);
|
||||
var to = new Date(event.to * 1000);
|
||||
|
||||
var next = Math.min(from || to, to || from);
|
||||
|
||||
var from_formatted = moment(from).format(
|
||||
'ddd Do MMMM YYYY' + (event.all_day ? '' : ', ' + DateTimeUtils.getDefaultTimeFormat()),
|
||||
);
|
||||
var to_formatted =
|
||||
(!event.type || event.type === 'event' || event.type === 'move') &&
|
||||
!isNaN(event.to) &&
|
||||
moment(to).format(
|
||||
(moment(to).format('D_M_YYYY') !== moment(from).format('D_M_YYYY')
|
||||
? 'ddd Do ' +
|
||||
(moment(to).format('M_YYYY') !== moment(from).format('M_YYYY')
|
||||
? 'MMMM ' + (moment(to).format('YYYY') !== moment(from).format('YYYY') ? 'YYYY' : '')
|
||||
: '') +
|
||||
(event.all_day ? '' : ', ')
|
||||
: '') + (event.all_day ? '' : DateTimeUtils.getDefaultTimeFormat()) || '[]',
|
||||
);
|
||||
|
||||
from_formatted = (from_formatted || '').replace(' ' + new Date().getFullYear(), '');
|
||||
to_formatted = (to_formatted || '').replace(' ' + new Date().getFullYear(), '');
|
||||
|
||||
var event_type =
|
||||
CalendarService.event_types_by_value[event.type] ||
|
||||
CalendarService.event_types_by_value['event'];
|
||||
var readonly = CalendarService.getIsReadonly(this.props.event);
|
||||
|
||||
return (
|
||||
<div className="eventModal event_details" style={{ padding: '16px' }}>
|
||||
<div className="title">
|
||||
{event.title || Languages.t('scenes.apps.drive.navigators.new_file.untitled')}
|
||||
</div>
|
||||
<div className="subtitle date">
|
||||
<Icon type="clock" />
|
||||
{from_formatted}
|
||||
{to_formatted && ' - ' + to_formatted}
|
||||
{' (' + moment(next).fromNow() + ')'}
|
||||
</div>
|
||||
|
||||
<div style={{ margin: '0 -16px' }}>
|
||||
<Tabs
|
||||
tabs={[
|
||||
{
|
||||
title: Languages.t('scenes.apps.calendar.modals.details_title', [], 'Détails'),
|
||||
render: (
|
||||
<div>
|
||||
<div className="bottom-margin">
|
||||
<div className="event_type">
|
||||
<Icon type={event_type.icon} />
|
||||
{event_type.text}
|
||||
</div>
|
||||
|
||||
<CalendarSelector
|
||||
readonly
|
||||
value={event.workspaces_calendars}
|
||||
openEventInWorkspace={workspace => {
|
||||
SearchService.select({
|
||||
type: 'event',
|
||||
event: this.props.event,
|
||||
workspace: workspace,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{event.description && (
|
||||
<div className="subtitle description bottom-margin">
|
||||
<Icon type="align-left-justify" style={{ marginRight: 4 }} />
|
||||
{event.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{event.location && (event.location || '').slice(0, 4) !== 'http' && (
|
||||
<div className="subtitle location bottom-margin">
|
||||
<Icon type="location-point" style={{ marginRight: 4 }} />
|
||||
{event.location}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{event.location && (event.location || '').slice(0, 4) === 'http' && (
|
||||
<div
|
||||
onClick={() => {
|
||||
var separator = '?';
|
||||
if (event.location.indexOf('?') > 0) {
|
||||
separator = '&';
|
||||
}
|
||||
window.open(
|
||||
event.location +
|
||||
separator +
|
||||
'twake_user=' +
|
||||
UserService.getCurrentUser().id +
|
||||
'&twake_group=' +
|
||||
WorkspacesService.currentGroupId,
|
||||
);
|
||||
}}
|
||||
className="subtitle location bottom-margin"
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
<Icon type="link" style={{ marginRight: 4 }} />
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a>
|
||||
{Languages.t('scenes.apps.calendar.video_link', [], 'Click to open link')}{' '}
|
||||
- {event.location}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(event.notifications || []).length > 0 && (
|
||||
<div className="subtitle reminders bottom-margin">
|
||||
<Icon type="bell" style={{ marginRight: 4 }} />
|
||||
{(event.notifications || []).length}{' '}
|
||||
{Languages.t('scenes.apps.calendar.reminders', [], 'rappel(s)')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/*
|
||||
<div className="text">
|
||||
{(event.notifications || []).length} rappel(s), événement {event.private?"privé":"public"} et marqué comme {event.available?"disponible":"occupé"}.
|
||||
</div>*/}
|
||||
|
||||
<div style={{ marginTop: '-16px' }} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: Languages.t(
|
||||
'scenes.apps.calendar.modals.participants_event',
|
||||
[],
|
||||
'Participants',
|
||||
),
|
||||
render: (
|
||||
<div className="small-x-margin small-bottom-margin">
|
||||
<Participants readOnly participants={event.participants} owner={event.owner} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!readonly && (
|
||||
<div style={{ marginTop: '-16px' }}>
|
||||
<div className="separator" />
|
||||
|
||||
<Button
|
||||
className="button medium danger medium"
|
||||
style={{ width: 'auto' }}
|
||||
onClick={() => {
|
||||
this.remove();
|
||||
}}
|
||||
>
|
||||
{Languages.t('scenes.apps.calendar.modals.remove_button', [], 'Supprimer')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="button medium secondary-light medium"
|
||||
style={{ width: 'auto', float: 'right' }}
|
||||
onClick={() => {
|
||||
CalendarService.edit(this.props.event);
|
||||
MediumPopupManager.open(
|
||||
<EventModification
|
||||
event={CalendarService.edited}
|
||||
collectionKey={this.props.collectionKey}
|
||||
/>,
|
||||
{ size: { width: 600 } },
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Languages.t(
|
||||
'scenes.apps.calendar.modals.modify_event_button',
|
||||
[],
|
||||
"Modifier l'évènement",
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{readonly && <div style={{ marginTop: '-16px' }} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import CalendarService from 'app/deprecated/Apps/Calendar/Calendar.js';
|
||||
import Input from 'components/inputs/input.jsx';
|
||||
import InputIcon from 'components/inputs/input-icon.jsx';
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import ReminderSelector from 'components/reminder-selector/reminder-selector.jsx';
|
||||
import Participants from './Part/Participants.jsx';
|
||||
import AlertManager from 'app/features/global/services/alert-manager-service';
|
||||
import DateSelector from './Part/DateSelector.jsx';
|
||||
import AttachmentPicker from 'components/attachment-picker/attachment-picker.jsx';
|
||||
import CalendarSelector from 'components/calendar/calendar-selector/calendar-selector.jsx';
|
||||
import Button from 'components/buttons/button.jsx';
|
||||
import Select from 'components/select/select.jsx';
|
||||
import WorkspaceService from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
import MediumPopupManager from 'app/components/modal/modal-manager';
|
||||
import Icon from 'app/components/icon/icon.jsx';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import './Modals.scss';
|
||||
|
||||
export default class EventModification extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
CalendarService.fullSizeModal = true;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MediumPopupManager.mountedComponent = this;
|
||||
}
|
||||
|
||||
save() {
|
||||
CalendarService.saveEdit(this.props.collectionKey);
|
||||
}
|
||||
|
||||
remove() {
|
||||
AlertManager.confirm(
|
||||
() => {
|
||||
CalendarService.remove(CalendarService.edited, this.props.collectionKey);
|
||||
},
|
||||
() => {},
|
||||
{
|
||||
text: Languages.t(
|
||||
'scenes.apps.calendar.modals.remove_event_text',
|
||||
[],
|
||||
"Supprimer l'événement ?",
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
onMediumPopupClose() {
|
||||
CalendarService.closePopups();
|
||||
}
|
||||
|
||||
change(key, value, notify) {
|
||||
this.props.event[key] = value;
|
||||
|
||||
if (notify || notify === undefined) {
|
||||
this.update_timeout && clearTimeout(this.update_timeout);
|
||||
this.update_timeout = setTimeout(() => {
|
||||
Collections.get('events').notify();
|
||||
}, 1000);
|
||||
this.setState({});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var event = this.props.event;
|
||||
var calendar_list = Collections.get('calendars').findBy({
|
||||
workspace_id: WorkspaceService.currentWorkspaceId,
|
||||
});
|
||||
|
||||
return (
|
||||
<PerfectScrollbar options={{ suppressScrollX: true }} style={{ padding: '16px' }}>
|
||||
<div className="eventModal event_modification">
|
||||
<Input
|
||||
autoFocus
|
||||
value={event.title || ''}
|
||||
placeholder={Languages.t('scenes.apps.calendar.modals.event_title_placeholder')}
|
||||
onChange={evt => {
|
||||
this.change('title', evt.target.value);
|
||||
}}
|
||||
className="full_width bottom-margin"
|
||||
big
|
||||
/>
|
||||
|
||||
<div className="bottom-margin date_and_type">
|
||||
<Select
|
||||
medium
|
||||
value={event.type || 'event'}
|
||||
onChange={value => {
|
||||
this.change('type', value);
|
||||
}}
|
||||
options={CalendarService.event_types}
|
||||
className="right-margin"
|
||||
/>
|
||||
<DateSelector
|
||||
event={event}
|
||||
onChange={(from, to, all_day, repetition_definition) => {
|
||||
this.change('from', from, false);
|
||||
this.change('to', to, false);
|
||||
this.change('all_day', all_day, false);
|
||||
this.change('repetition_definition', repetition_definition);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex' }} className="full_width">
|
||||
<div style={{ flex: 1, display: 'flex' }}>
|
||||
<InputIcon
|
||||
icon={(event.location || '').slice(0, 4) == 'http' ? 'link' : 'location-point'}
|
||||
medium
|
||||
value={event.location || ''}
|
||||
placeholder={Languages.t('scenes.apps.calendar.modals.event_adresse_placeholder')}
|
||||
onChange={evt => {
|
||||
this.change('location', evt.target.value);
|
||||
}}
|
||||
className="full_width bottom-margin right-margin"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="button medium default bottom-margin"
|
||||
onClick={() => {
|
||||
this.change(
|
||||
'location',
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
'/bundle/connectors/jitsi/call/twake_event_' +
|
||||
(event.front_id || '').replace(/-/g, '_') +
|
||||
'__' +
|
||||
(event.front_id || '').replace(/-/g, '_'),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon type="video" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<InputIcon
|
||||
autoHeight
|
||||
icon="align-left-justify"
|
||||
medium
|
||||
value={event.description || ''}
|
||||
placeholder={Languages.t(
|
||||
'scenes.apps.calendar.modals.event_description_placeholder',
|
||||
[],
|
||||
'Description',
|
||||
)}
|
||||
onChange={evt => {
|
||||
this.change('description', evt.target.value);
|
||||
}}
|
||||
className="full_width bottom-margin"
|
||||
/>
|
||||
|
||||
{/*
|
||||
<span className="right-margin">
|
||||
<Checkbox small value={event.available} onChange={(value)=>{this.change("available", value)}} label="Afficher comme disponible"/>
|
||||
</span>
|
||||
<Checkbox small value={event.private} onChange={(value)=>{this.change("private", value)}} label="Événement privé" />
|
||||
<br/>
|
||||
*/}
|
||||
|
||||
<CalendarSelector
|
||||
medium
|
||||
value={event.workspaces_calendars || []}
|
||||
onChange={workspaces_calendars => {
|
||||
this.change('workspaces_calendars', workspaces_calendars);
|
||||
}}
|
||||
calendarList={calendar_list}
|
||||
className=""
|
||||
/>
|
||||
|
||||
<div className="separator" />
|
||||
|
||||
<Participants
|
||||
style={{ margin: 0 }}
|
||||
participants={event.participants}
|
||||
owner={event.owner}
|
||||
onChange={user_id_or_mail => this.change('participants', user_id_or_mail)}
|
||||
/>
|
||||
|
||||
<div className="separator" />
|
||||
|
||||
<div className="bottom-margin">
|
||||
<b>{Languages.t('scenes.apps.tasks.modals.attachments')}</b>
|
||||
</div>
|
||||
|
||||
<AttachmentPicker
|
||||
attachments={event.attachments}
|
||||
onChange={attachments => this.change('attachments', attachments)}
|
||||
/>
|
||||
|
||||
<div className="separator" />
|
||||
|
||||
<div className="bottom-margin">
|
||||
<b>{Languages.t('scenes.apps.calendar.modals.reminders')}</b>
|
||||
</div>
|
||||
|
||||
<ReminderSelector
|
||||
reminders={event.notifications || []}
|
||||
onChange={reminders => this.change('notifications', reminders)}
|
||||
/>
|
||||
|
||||
<div className="separator" />
|
||||
|
||||
<Button
|
||||
className="button medium danger medium"
|
||||
style={{ width: 'auto' }}
|
||||
onClick={() => {
|
||||
this.remove();
|
||||
}}
|
||||
>
|
||||
{Languages.t('scenes.apps.calendar.modals.remove_event_button')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="button medium btn-primary medium"
|
||||
style={{ width: 'auto', float: 'right' }}
|
||||
onClick={() => {
|
||||
this.save();
|
||||
}}
|
||||
>
|
||||
{Languages.t('general.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</PerfectScrollbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
.eventModal {
|
||||
.title {
|
||||
font-size: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.date.subtitle,
|
||||
.location.subtitle,
|
||||
.reminders.subtitle {
|
||||
color: var(--grey-dark);
|
||||
}
|
||||
|
||||
.date_and_type {
|
||||
display: flex;
|
||||
.select {
|
||||
min-width: 25%;
|
||||
flex: 1;
|
||||
max-width: 140px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.date_selector_full {
|
||||
& > div {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.participants > .menu-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.event_type {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
line-height: 32px;
|
||||
vertical-align: middle;
|
||||
background: var(--white);
|
||||
border-radius: var(--border-radius-base);
|
||||
border: 1px solid var(--grey-background);
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
padding-left: 4px;
|
||||
.icon-unicon {
|
||||
color: var(--grey-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/* eslint-disable react/no-direct-mutation-state */
|
||||
import React, { Component } from 'react';
|
||||
import DateSelectorInput from 'components/calendar/date-picker.jsx';
|
||||
import TimeSelector from 'components/calendar/time-selector.jsx';
|
||||
import Checkbox from 'app/components/inputs/deprecated_checkbox.jsx';
|
||||
import Icon from 'app/components/icon/icon.jsx';
|
||||
import './DateSelector.scss';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
|
||||
export default class DateSelector extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: {},
|
||||
};
|
||||
|
||||
this.oldProps = JSON.stringify(props.event);
|
||||
this.updateFromProps(props);
|
||||
}
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (this.oldProps !== JSON.stringify(nextProps.event)) {
|
||||
this.oldProps = JSON.stringify(nextProps.event);
|
||||
this.updateFromProps(nextProps);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
change(key, value) {
|
||||
// eslint-disable-next-line react/no-direct-mutation-state
|
||||
this.state.data[key] = value;
|
||||
|
||||
if (key === 'from') {
|
||||
if (
|
||||
this.state.data.to < this.state.data.from ||
|
||||
(this.state.data.to - this.state.data.from > 60 * 60 * 24 * 2 && !this.state.data.all_day)
|
||||
) {
|
||||
this.state.data.to = this.state.data.from + (this.duration || 60 * 60);
|
||||
}
|
||||
}
|
||||
if (key === 'to') {
|
||||
if (
|
||||
this.state.data.to < this.state.data.from ||
|
||||
(this.state.data.to - this.state.data.from > 60 * 60 * 24 * 2 && !this.state.data.all_day)
|
||||
) {
|
||||
this.state.data.from = this.state.data.to - (this.duration || 60 * 60);
|
||||
}
|
||||
}
|
||||
if (key === 'all_day') {
|
||||
if (
|
||||
this.state.data.to - this.state.data.from > 60 * 60 * 24 * 2 &&
|
||||
!this.state.data.all_day
|
||||
) {
|
||||
this.state.data.to = this.state.data.from + Math.min(this.duration || 60 * 60, 60 * 60);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.data.to > this.state.data.from) {
|
||||
this.duration = this.state.data.to - this.state.data.from;
|
||||
}
|
||||
|
||||
this.setState({});
|
||||
this.update();
|
||||
}
|
||||
update() {
|
||||
var from = this.state.data.from;
|
||||
var to = this.state.data.to;
|
||||
|
||||
if (this.props.event.type === 'deadline' || this.props.event.type === 'remind') {
|
||||
to = from + 15 * 60;
|
||||
}
|
||||
|
||||
if (to < from) {
|
||||
to = from + 60 * 60;
|
||||
}
|
||||
|
||||
var all_day = this.state.data.all_day;
|
||||
this.props.onChange && this.props.onChange(from, to, all_day, null);
|
||||
}
|
||||
updateFromProps(props) {
|
||||
this.state.data.from = props.event.from;
|
||||
this.state.data.to = props.event.to;
|
||||
this.state.data.all_day = props.event.all_day;
|
||||
|
||||
if (this.state.data.to > this.state.data.from) {
|
||||
this.duration = this.state.data.to - this.state.data.from;
|
||||
}
|
||||
}
|
||||
render() {
|
||||
var event = this.props.event;
|
||||
return (
|
||||
<div className="calendar_selector">
|
||||
<div className="date_selector_full">
|
||||
<span className="clock_part" style={{ verticalAlign: 'top', display: 'inline-block' }}>
|
||||
<Icon type="clock" className="icon_clock" />
|
||||
</span>
|
||||
<div className="start">
|
||||
<DateSelectorInput
|
||||
ts={this.state.data.from}
|
||||
onChangeBlur={value => this.change('from', value)}
|
||||
className=""
|
||||
/>
|
||||
{!event.all_day && (
|
||||
<TimeSelector
|
||||
ts={this.state.data.from}
|
||||
onChangeBlur={value => this.change('from', value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(!event.type || event.type === 'event' || event.type === 'move') && [
|
||||
<span className="middle" key={`title-${event}`}>
|
||||
{Languages.t('scenes.apps.calendar.event_view.article_until')}
|
||||
</span>,
|
||||
<div className="end" key={`selector-${event}`}>
|
||||
{!event.all_day && (
|
||||
<TimeSelector
|
||||
ts={this.state.data.to}
|
||||
onChangeBlur={value => this.change('to', value)}
|
||||
className=""
|
||||
/>
|
||||
)}
|
||||
<DateSelectorInput
|
||||
ts={this.state.data.to}
|
||||
onChangeBlur={value => this.change('to', value)}
|
||||
/>
|
||||
</div>,
|
||||
]}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Checkbox
|
||||
small
|
||||
value={event.all_day}
|
||||
onChange={value => {
|
||||
this.change('all_day', value);
|
||||
}}
|
||||
label={Languages.t(
|
||||
'scenes.apps.calendar.event_edition.checkbox_all_day',
|
||||
[],
|
||||
'All day',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
.date_selector_full {
|
||||
display: inline-flex;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
border-radius: var(--border-radius-base);
|
||||
background: var(--grey-background);
|
||||
max-width: 360px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.clock_part {
|
||||
text-align: center;
|
||||
width: 6%;
|
||||
max-width: 32px;
|
||||
min-width: 20px;
|
||||
}
|
||||
& > .middle {
|
||||
width: 10%;
|
||||
max-width: 32px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
padding: 0px 4px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
& > .start,
|
||||
& > .end {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
max-width: 150px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
& > .time_selector {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
& > .date_selector {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon_clock {
|
||||
color: var(--grey-dark);
|
||||
margin-top: 12px;
|
||||
display: inline-block;
|
||||
margin-right: -4px;
|
||||
}
|
||||
.input.medium {
|
||||
text-align: center;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import UserListManager from 'components/user-list-manager-depreciated/user-list-manager';
|
||||
import Menu from 'components/menus/menu.jsx';
|
||||
|
||||
export default class Participants extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="participants">
|
||||
<div className="bottom-margin">
|
||||
<b>{Languages.t('scenes.apps.calendar.modals.part.participants', [], 'Participants')}</b>
|
||||
</div>
|
||||
|
||||
<div className="menu-list">
|
||||
<UserListManager
|
||||
showAddMe
|
||||
showAddAll
|
||||
readOnly={this.props.readOnly}
|
||||
canRemoveMyself
|
||||
noPlaceholder
|
||||
users={(this.props.participants || []).map(participant => {
|
||||
return { id: participant.user_id_or_mail };
|
||||
})}
|
||||
scope="workspace"
|
||||
allowMails
|
||||
onUpdate={ids_mails => {
|
||||
this.props.onChange &&
|
||||
this.props.onChange(
|
||||
ids_mails.map(id => {
|
||||
return { user_id_or_mail: id };
|
||||
}),
|
||||
);
|
||||
Menu.closeAll();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import Menu from 'components/menus/menu.jsx';
|
||||
import Button from 'components/buttons/button.jsx';
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import RouterService from 'app/features/router/services/router-service';
|
||||
import CalendarSelector from 'components/calendar/calendar-selector/calendar-selector.jsx';
|
||||
import AccessRightsService from 'app/features/workspace-members/services/workspace-members-access-rights-service';
|
||||
import WorkspaceService from 'app/deprecated/workspaces/workspaces';
|
||||
|
||||
import './calendar.scss';
|
||||
|
||||
export default class UnconfiguredTab extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
i18n: Languages,
|
||||
selected: [],
|
||||
};
|
||||
|
||||
Languages.addListener(this);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
Languages.removeListener(this);
|
||||
}
|
||||
initInCalendars() {
|
||||
if (this.props.saveTab) this.props.saveTab({ calendars: this.state.selected });
|
||||
Menu.closeAll();
|
||||
}
|
||||
|
||||
render() {
|
||||
var calendar_list = Collections.get('calendars').findBy({
|
||||
workspace_id: RouterService.getStateFromRoute().workspaceId,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="unconfigured_tab">
|
||||
<div className="title">{this.props.tab.name}</div>
|
||||
<div className="text" style={{ opacity: 0.5 }}>
|
||||
{Languages.t(
|
||||
'scenes.apps.calendar.unconfigured_tab',
|
||||
[],
|
||||
"Cet onglet n'est pas encore configuré.",
|
||||
)}
|
||||
</div>
|
||||
|
||||
{AccessRightsService.getCompanyLevel(WorkspaceService.currentGroupId) !== 'guest' && (
|
||||
<>
|
||||
<br />
|
||||
<CalendarSelector
|
||||
allowMultiple
|
||||
medium
|
||||
value={this.state.selected}
|
||||
onChange={workspaces_calendars => {
|
||||
this.setState({ selected: workspaces_calendars });
|
||||
}}
|
||||
calendarList={calendar_list || []}
|
||||
className=""
|
||||
/>
|
||||
|
||||
<br />
|
||||
|
||||
{this.state.selected.length > 0 && (
|
||||
<Button
|
||||
className="button medium"
|
||||
onClick={() => this.initInCalendars()}
|
||||
style={{ width: 'auto' }}
|
||||
>
|
||||
{Languages.t('general.continue', [], 'Continuer')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import Browser from './browser';
|
||||
import { SelectorModal } from './modals/selector';
|
||||
import TwakeTabConfiguration from './twake-tab-configuration';
|
||||
|
||||
export type EmbedContext = {
|
||||
companyId?: string;
|
||||
workspaceId?: string;
|
||||
channelId?: string;
|
||||
tabId?: string;
|
||||
};
|
||||
|
||||
export default ({
|
||||
initialParentId,
|
||||
context,
|
||||
inPublicSharing,
|
||||
}: {
|
||||
initialParentId?: string;
|
||||
context?: EmbedContext;
|
||||
inPublicSharing?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<SelectorModal />
|
||||
<Drive
|
||||
initialParentId={initialParentId}
|
||||
context={context}
|
||||
inPublicSharing={inPublicSharing}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Drive = ({
|
||||
initialParentId,
|
||||
context,
|
||||
inPublicSharing,
|
||||
}: {
|
||||
initialParentId?: string;
|
||||
context?: EmbedContext;
|
||||
inPublicSharing?: boolean;
|
||||
}) => {
|
||||
if (context?.tabId) {
|
||||
return <TwakeTabConfiguration context={context} />;
|
||||
}
|
||||
|
||||
return <Browser initialParentId={initialParentId} inPublicSharing={inPublicSharing} />;
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Modal } from 'app/atoms/modal';
|
||||
import { Info } from 'app/atoms/text';
|
||||
import Button from 'app/components/buttons/button';
|
||||
import { useDriveTwakeTab } from 'app/features/drive-twake/hooks/use-drive-twake-tab';
|
||||
import { useDriveItem } from 'app/features/drive/hooks/use-drive-item';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { EmbedContext } from '.';
|
||||
import Browser from './browser';
|
||||
import { SelectorModalAtom } from './modals/selector';
|
||||
|
||||
export default ({ context }: { context?: EmbedContext }) => {
|
||||
const { tab, setTab, loading } = useDriveTwakeTab(context?.channelId || '', context?.tabId || '');
|
||||
const { item, loading: itemLoading, refresh } = useDriveItem(tab?.item_id || '');
|
||||
const setSelectorModalState = useSetRecoilState(SelectorModalAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab?.item_id) refresh(tab!.item_id);
|
||||
}, [tab?.item_id, refresh]);
|
||||
|
||||
// If nothing is configured, then show the selector and when selected the folder will give access to the whole channel
|
||||
const modalOpen = (!tab || !item) && !loading && !itemLoading;
|
||||
const isConfigured = tab && item;
|
||||
|
||||
useEffect(() => {
|
||||
if (modalOpen) {
|
||||
setSelectorModalState({
|
||||
open: true,
|
||||
parent_id: 'root',
|
||||
mode: 'move',
|
||||
title: `Select what folder this tab should display`,
|
||||
onSelected: async ids => {
|
||||
await setTab(ids[0], 'write');
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [modalOpen]);
|
||||
|
||||
if (!item && !tab) return <></>;
|
||||
|
||||
// If configured then show the content of the tab and forward the fact that the access is done through a specific channel
|
||||
return (
|
||||
<div>
|
||||
{isConfigured && (
|
||||
<Browser
|
||||
initialParentId={item.id}
|
||||
twakeTabContextToken={context?.channelId + '+' + context?.tabId}
|
||||
/>
|
||||
)}
|
||||
{!isConfigured && !loading && !(tab?.item_id && itemLoading) && (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Info>This Documents tabs is not configured yet.</Info>
|
||||
<br />
|
||||
<Button
|
||||
theme="outlined"
|
||||
className="mt-4"
|
||||
onClick={() =>
|
||||
setSelectorModalState({
|
||||
open: true,
|
||||
parent_id: 'root',
|
||||
mode: 'move',
|
||||
title: `Select what folder this tab should display`,
|
||||
onSelected: async ids => {
|
||||
await setTab(ids[0], 'write');
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ChannelType } from 'app/features/channels/types/channel';
|
||||
import Messages from './messages';
|
||||
import { ViewConfiguration } from 'app/features/router/services/app-view-service';
|
||||
|
||||
import './messages.scss';
|
||||
|
||||
type Props = {
|
||||
channel: ChannelType;
|
||||
options: ViewConfiguration;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instanciate a Messages component with a good unicity key
|
||||
*/
|
||||
export default (props: Props) => {
|
||||
if (!props.channel) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Messages
|
||||
channel={props.channel}
|
||||
options={props.options}
|
||||
key={`${props.options?.context?.threadId}${props.channel?.id}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,216 +0,0 @@
|
||||
.new-thread-button {
|
||||
.message {
|
||||
align-items: center;
|
||||
color: var(--secondary);
|
||||
.plus-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
.thread.with-block {
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
&:active {
|
||||
.thread.with-block {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-thread {
|
||||
.message-input {
|
||||
.upload-zone-centerer {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.unfocused {
|
||||
opacity: 0.5;
|
||||
.input-options {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete {
|
||||
div.input {
|
||||
background: #fff;
|
||||
textarea.input {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
.input.small {
|
||||
margin-top: -2px;
|
||||
padding: 0;
|
||||
}
|
||||
.input.autoheight_container.small textarea {
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.editorview-submit {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
|
||||
.editor {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
display: flex;
|
||||
align-self: flex-start;
|
||||
color: var(--primary);
|
||||
opacity: 50%;
|
||||
cursor: not-allowed;
|
||||
|
||||
&.disabled{
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:not(.disabled):hover {
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
opacity: unset;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.counter-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.input-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
margin-top: 8px;
|
||||
|
||||
button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 4px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--grey-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-options {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
|
||||
.option {
|
||||
color: var(--grey-light);
|
||||
|
||||
&:first-child {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
&:not(.disabled):hover {
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
cursor: pointer;
|
||||
color: var(--grey-dark);
|
||||
}
|
||||
|
||||
.messages-input-app-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
}
|
||||
|
||||
.richtext {
|
||||
&.selected {
|
||||
background: var(--primary-background);
|
||||
|
||||
.option {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-zone-centerer {
|
||||
margin: 0 auto;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ephemerals {
|
||||
text-align: left;
|
||||
|
||||
.thread-section {
|
||||
width: 100%;
|
||||
padding-bottom: 8px;
|
||||
.message {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ephemerals_text {
|
||||
font-size: 12px;
|
||||
color: var(--grey-dark);
|
||||
padding-bottom: 12px;
|
||||
max-width: 1362px;
|
||||
width: calc(100% - 48px);
|
||||
display: inline-flex;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.small-section.alinea {
|
||||
.ephemerals .thread-container .sender-space {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.attached-files-container {
|
||||
width: 100%;
|
||||
|
||||
.attached-file {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { EditorState } from 'draft-js';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
import FileUploadAPIClient from 'app/features/files/api/file-upload-api-client';
|
||||
import InputOptions from './parts/InputOptions';
|
||||
import EphemeralMessages from './parts/EphemeralMessages';
|
||||
import MessageEditorsManager from 'app/features/messages/services/message-editor-service-factory';
|
||||
import MenusManager from 'app/components/menus/menus-manager.jsx';
|
||||
import PendingAttachments from './parts/PendingAttachments';
|
||||
import RichTextEditorStateService from 'app/components/rich-text-editor/editor-state-service';
|
||||
import { EditorView } from 'app/components/rich-text-editor';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { TextCount, TextCountService } from 'app/components/rich-text-editor/text-count/';
|
||||
import UploadZone from 'app/components/uploads/upload-zone';
|
||||
import Workspaces from 'app/deprecated/workspaces/workspaces';
|
||||
import { FileType } from 'app/features/files/types/file';
|
||||
import { useUploadZones } from 'app/features/files/hooks/use-upload-zones';
|
||||
import { useMessageEditor } from 'app/features/messages/hooks/use-message-editor';
|
||||
import useRouterCompany from 'app/features/router/hooks/use-router-company';
|
||||
import { delayRequest } from 'app/features/global/utils/managedSearchRequest';
|
||||
import { useChannel } from 'app/features/channels/hooks/use-channel';
|
||||
import {
|
||||
useChannelWritingActivityEmit,
|
||||
useWritingDetector,
|
||||
} from 'app/features/channels/hooks/use-channel-writing-activity';
|
||||
import {
|
||||
getCompanyApplication,
|
||||
getCompanyApplications,
|
||||
} from 'app/features/applications/state/company-applications';
|
||||
import AlertManager from 'app/features/global/services/alert-manager-service';
|
||||
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
|
||||
import './input.scss';
|
||||
import { Application } from 'app/features/applications/types/application';
|
||||
import MessageExternalFilePicker from './parts/MessageExternalFilePicker';
|
||||
import FilePicker from 'app/components/drive/file-picker/file-picker';
|
||||
import { MessageFileType } from 'app/features/messages/types/message';
|
||||
import { ChannelType } from 'app/features/channels/types/channel';
|
||||
import { useMessageQuoteReply } from 'app/features/messages/hooks/use-message-quote-reply';
|
||||
import QuotedMessage from 'app/components/quoted-message/quoted-message';
|
||||
import { UpIcon, PlusIcon } from 'app/atoms/icons-agnostic';
|
||||
|
||||
type Props = {
|
||||
messageId?: string;
|
||||
channelId?: string;
|
||||
threadId: string;
|
||||
collectionKey?: string;
|
||||
onResize?: (evt: any) => void;
|
||||
onEscape?: (evt: any) => void;
|
||||
onFocus?: () => void;
|
||||
ref?: (node: any) => void;
|
||||
onSend?: (text: string) => void;
|
||||
onChange?: (editorState: EditorState) => void;
|
||||
triggerApp?: (app: any, from_icon: any, evt: any) => void;
|
||||
localStorageIdentifier?: string;
|
||||
disableApps?: boolean;
|
||||
context?: string; //Main input or response input (empty string)
|
||||
format?: 'markdown' | 'raw';
|
||||
editorPlugins?: Array<string>;
|
||||
editorState?: EditorState;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const { channel } = useChannel(props.channelId || '');
|
||||
const companyId = useRouterCompany();
|
||||
|
||||
const {
|
||||
editor,
|
||||
setValue,
|
||||
setFiles,
|
||||
send,
|
||||
key: editorId,
|
||||
} = useMessageEditor({
|
||||
companyId,
|
||||
workspaceId: channel?.workspace_id || '',
|
||||
channelId: props.channelId,
|
||||
threadId: props.threadId,
|
||||
messageId: props.messageId,
|
||||
});
|
||||
|
||||
const { message: parentMessage } = useMessage({
|
||||
companyId,
|
||||
threadId: props.threadId,
|
||||
id: props.threadId,
|
||||
});
|
||||
|
||||
const {
|
||||
isActive: isBeingQuoted,
|
||||
close,
|
||||
message: messageBeingQuoted,
|
||||
} = useMessageQuoteReply(props.channelId || '');
|
||||
|
||||
const { upload, clear: clearUploads } = useUploadZones(editorId);
|
||||
const format = props.format || 'markdown';
|
||||
const editorRef = useRef<EditorView>(null);
|
||||
const submitRef = useRef<HTMLDivElement>(null);
|
||||
const [hasEphemeralMessage, setHasEphemeralMessage] = useState(false);
|
||||
const messageEditorService = MessageEditorsManager.get(props.channelId || '');
|
||||
|
||||
const editorPlugins = props.editorPlugins || ['emoji', 'mention', 'channel', 'command'];
|
||||
const [editorState, setEditorState] = useState(() =>
|
||||
RichTextEditorStateService.get(editorId, { plugins: editorPlugins }),
|
||||
);
|
||||
const [isTooLong, setTooLong] = useState(false);
|
||||
const [inPlus, setInPlus] = useState(false);
|
||||
|
||||
const { iAmWriting } = useChannelWritingActivityEmit(props.channelId || '', props.threadId);
|
||||
|
||||
const { onKeydown: onKeydownRealtimeListener } = useWritingDetector();
|
||||
|
||||
useEffect(() => {
|
||||
setTooLong(TextCountService.getStats(editorState).isTooLong);
|
||||
}, [editorState]);
|
||||
|
||||
useEffect(() => {
|
||||
focusEditor();
|
||||
(async () => {
|
||||
if (editor.value && editor.value.length) {
|
||||
setEditorState(
|
||||
RichTextEditorStateService.get(editorId, {
|
||||
plugins: editorPlugins,
|
||||
clearIfExists: true,
|
||||
initialContent: RichTextEditorStateService.getDataParser(editorPlugins).fromString(
|
||||
editor.value,
|
||||
format,
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const disable_app: any = {};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.editorState && props.editorState !== editorState) {
|
||||
setEditorState(props.editorState);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.editorState]);
|
||||
|
||||
const getContentOutput = (editorState: EditorState) => {
|
||||
return RichTextEditorStateService.getDataParser(editorPlugins).toString(editorState, format);
|
||||
};
|
||||
|
||||
const onSend = async () => {
|
||||
const content = getContentOutput(editorState);
|
||||
setValue(content);
|
||||
|
||||
if (props.onSend) {
|
||||
props.onSend(content);
|
||||
return;
|
||||
}
|
||||
|
||||
//Sending commands
|
||||
if (content.indexOf('/') === 0 && !props.messageId) {
|
||||
let app: any = null;
|
||||
const app_name = content.split(' ')[0].slice(1);
|
||||
// eslint-disable-next-line array-callback-return
|
||||
getCompanyApplications(companyId).map((_app: any) => {
|
||||
if (_app?.identity?.code === app_name) {
|
||||
app = _app;
|
||||
}
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
AlertManager.alert(() => {}, {
|
||||
text: Languages.t('services.apps.messages.no_command_possible', [content, app_name]),
|
||||
title: Languages.t('services.apps.messages.no_app'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
command: content.split(' ').slice(1).join(' '),
|
||||
channel: channel,
|
||||
thread: parentMessage?.id ? parentMessage : null,
|
||||
};
|
||||
|
||||
WorkspacesApps.notifyApp(app.id, 'action', 'command', data);
|
||||
setEditorState(RichTextEditorStateService.clear(editorId).get(editorId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (content || editor.files.length > 0) {
|
||||
setEditorState(RichTextEditorStateService.clear(editorId).get(editorId));
|
||||
clearUploads();
|
||||
await send();
|
||||
}
|
||||
};
|
||||
|
||||
const triggerApp = (app: any, from_icon: any, evt: any) => {
|
||||
if (disable_app[app.id] && new Date().getTime() - disable_app[app.id] < 1000) {
|
||||
return;
|
||||
}
|
||||
disable_app[app.id] = new Date().getTime();
|
||||
|
||||
const threadId = parentMessage.id;
|
||||
|
||||
if (app?.identity?.code === 'twake_drive') {
|
||||
const menu = [];
|
||||
const has_drive_app = getCompanyApplication(app.id);
|
||||
|
||||
if (has_drive_app) {
|
||||
menu.push({
|
||||
type: 'react-element',
|
||||
reactElement: () => {
|
||||
let fileHandler: (file: MessageFileType) => void;
|
||||
return (
|
||||
<MessageExternalFilePicker
|
||||
channel={channel as ChannelType}
|
||||
threadId={threadId}
|
||||
setHandler={handler => (fileHandler = handler)}
|
||||
>
|
||||
<FilePicker
|
||||
mode="select_file"
|
||||
onChoose={(file: any) => {
|
||||
if (fileHandler)
|
||||
fileHandler({
|
||||
metadata: {
|
||||
external_id: {
|
||||
id: file.id,
|
||||
workspace_id: file.workspace_id,
|
||||
parent_id: file.parent_id,
|
||||
company_id: channel?.company_id || '',
|
||||
},
|
||||
source: 'drive',
|
||||
name: file.name,
|
||||
size: parseInt(file.size),
|
||||
mime: FileUploadAPIClient.extensionToMime(file.extension),
|
||||
thumbnails: file.preview_has_been_generated
|
||||
? [
|
||||
{
|
||||
mime: 'image/png',
|
||||
url: file.preview_link,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MessageExternalFilePicker>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
MenusManager.openMenu(menu, { x: evt.clientX, y: evt.clientY }, 'center', {});
|
||||
return;
|
||||
}
|
||||
|
||||
if ((app as Application).display?.twake?.chat?.input) {
|
||||
WorkspacesApps.openAppPopup(app.id);
|
||||
}
|
||||
|
||||
const data = {
|
||||
channel,
|
||||
parent_message: parentMessage?.id ? parentMessage : null,
|
||||
from_icon: from_icon,
|
||||
};
|
||||
|
||||
WorkspacesApps.notifyApp(app.id, 'action', 'open', data);
|
||||
};
|
||||
|
||||
const focus = () => {
|
||||
messageEditorService.openEditor(props.threadId || '', props.messageId || '', props.context);
|
||||
};
|
||||
|
||||
const focusEditor = () => {
|
||||
requestAnimationFrame(() => editorRef.current?.focus());
|
||||
};
|
||||
|
||||
const setRichTextEditorState = (editorState: EditorState): void => {
|
||||
setEditorState(editorState);
|
||||
RichTextEditorStateService.set(editorId, editorState);
|
||||
};
|
||||
|
||||
const isEmpty = (): boolean => {
|
||||
return (
|
||||
editorState.getCurrentContent().getPlainText().trim().length === 0 && !editor.files.length
|
||||
);
|
||||
};
|
||||
|
||||
const onUpArrow = (): void => {
|
||||
if (isEmpty()) {
|
||||
//TODO
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = async (newEditorState: EditorState) => {
|
||||
const statsAfter = TextCountService.getStats(newEditorState);
|
||||
const statsBefore = TextCountService.getStats(editorState);
|
||||
if (statsAfter.length > statsBefore.length)
|
||||
onKeydownRealtimeListener(state => iAmWriting(state));
|
||||
|
||||
//Delay request make the input faster (getContentOutput is a heavy call)
|
||||
delayRequest(`editor-${editorId}`, async () => {
|
||||
setValue(getContentOutput(newEditorState));
|
||||
});
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(newEditorState);
|
||||
return;
|
||||
}
|
||||
setRichTextEditorState(newEditorState);
|
||||
};
|
||||
|
||||
const setUploadZoneRef = (node: UploadZone): void =>
|
||||
messageEditorService.setUploadZone(props.messageId || props.threadId || '', node);
|
||||
|
||||
const onUploaded = (file: FileType) =>
|
||||
messageEditorService.onAddAttachment(props.messageId || props.threadId, file);
|
||||
|
||||
const onDragEnter = (): void => {
|
||||
messageEditorService.getUploadZone(props.threadId);
|
||||
};
|
||||
|
||||
const getFilesLimit = () => {
|
||||
const attachements = messageEditorService.getAttachements(editorId) || [];
|
||||
const limit = messageEditorService.ATTACHEMENTS_LIMIT;
|
||||
|
||||
return attachements.length ? limit - attachements.length : limit;
|
||||
};
|
||||
|
||||
const onAddFiles = async (files: File[]) => {
|
||||
await upload(files);
|
||||
};
|
||||
|
||||
const onFilePaste = (blobs: Blob[]) => {
|
||||
if (blobs.length > 0) {
|
||||
const file = new File(
|
||||
[blobs[0]],
|
||||
'pasted_' +
|
||||
new Date()
|
||||
.toISOString()
|
||||
.replaceAll(/(Z|\.[0-9]+)/gm, '')
|
||||
.replace(/T/, '_') +
|
||||
'.png',
|
||||
{
|
||||
type: 'image/png',
|
||||
},
|
||||
);
|
||||
upload([file]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
focusEditor();
|
||||
}, [messageBeingQuoted]);
|
||||
|
||||
const disabled = isEmpty() || isTooLong;
|
||||
return (
|
||||
<div className={'message-input w-full'} ref={props.ref} onClick={() => focus()}>
|
||||
{isBeingQuoted && <QuotedMessage onClose={() => close()} />}
|
||||
<UploadZone
|
||||
className="upload-zone-centerer"
|
||||
ref={setUploadZoneRef}
|
||||
disableClick
|
||||
parent={''}
|
||||
driveCollectionKey={props.collectionKey}
|
||||
uploadOptions={{ workspace_id: Workspaces.currentWorkspaceId, detached: true }}
|
||||
onUploaded={onUploaded}
|
||||
onDragEnter={onDragEnter}
|
||||
multiple={true}
|
||||
allowPaste={true}
|
||||
filesLimit={getFilesLimit()}
|
||||
onAddFiles={onAddFiles}
|
||||
>
|
||||
<EphemeralMessages
|
||||
channelId={props.channelId || ''}
|
||||
workspaceId={channel?.workspace_id || ''}
|
||||
threadId={props.threadId}
|
||||
onHasEphemeralMessage={() => {
|
||||
if (!hasEphemeralMessage) {
|
||||
setHasEphemeralMessage(true);
|
||||
}
|
||||
}}
|
||||
onNotEphemeralMessage={() => {
|
||||
if (hasEphemeralMessage) {
|
||||
setHasEphemeralMessage(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{!hasEphemeralMessage && (
|
||||
<div className="editorview-submit flex flex-row items-center px-1 relative">
|
||||
<div className="absolute -bottom-2 right-8">
|
||||
<TextCount editorState={editorState} displayOnlyAfterThresold={true} />
|
||||
</div>
|
||||
|
||||
{!props.messageId && (
|
||||
<div className="mr-2 self-start mt-[6px]">
|
||||
<PlusIcon
|
||||
onClick={() => {
|
||||
setInPlus(!inPlus);
|
||||
}}
|
||||
className={
|
||||
'cursor-pointer text-blue-500 hover:text-blue-600 w-5 h-5 transition-transform ' +
|
||||
(inPlus ? ' rotate-45 ' : '')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<EditorView
|
||||
ref={editorRef}
|
||||
onChange={editorState => {
|
||||
onChange(editorState);
|
||||
}}
|
||||
clearOnSubmit={true}
|
||||
outputFormat={format}
|
||||
plugins={editorPlugins}
|
||||
editorState={editorState}
|
||||
onSubmit={() => onSend()}
|
||||
onUpArrow={() => onUpArrow()}
|
||||
onFilePaste={onFilePaste}
|
||||
placeholder={Languages.t('scenes.apps.messages.input.placeholder')}
|
||||
/>
|
||||
{!props.messageId && (
|
||||
<Tooltip
|
||||
title={Languages.t('scenes.apps.messages.input.send_message')}
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
ref={submitRef}
|
||||
className={classNames('ml-2 submit-button self-start mt-0.5', {
|
||||
disabled: disabled,
|
||||
scale: !disabled,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (!isEmpty() && !isTooLong) {
|
||||
onSend();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<UpIcon className="text-blue-500 hover:text-blue-600 w-7 h-7" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasEphemeralMessage && !props.messageId && (
|
||||
<div className={'transition-all ' + (inPlus ? 'h-8 opacity-1' : 'h-0 opacity-0')}>
|
||||
<InputOptions
|
||||
isEmpty={isEmpty()}
|
||||
channelId={props.channelId || ''}
|
||||
threadId={props.threadId}
|
||||
onSend={() => onSend()}
|
||||
triggerApp={(app, fromIcon, evt) => triggerApp(app, fromIcon, evt)}
|
||||
onAddEmoji={emoji => editorRef.current?.insertCommand('EMOJI', emoji)}
|
||||
richTextEditorState={editorState}
|
||||
onRichTextChange={editorState => setRichTextEditorState(editorState)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PendingAttachments
|
||||
zoneId={editorId}
|
||||
initialValue={editor.files || []}
|
||||
onChange={list => setFiles(list)}
|
||||
/>
|
||||
</UploadZone>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { PlusCircle } from 'react-feather';
|
||||
|
||||
import Thread from '../parts/thread';
|
||||
import ThreadSection from '../parts/thread-section';
|
||||
import Input from './input';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { ViewContext } from 'app/views/client/main-view/MainContent';
|
||||
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
|
||||
|
||||
import './input.scss';
|
||||
|
||||
type Props = {
|
||||
useButton?: boolean;
|
||||
collectionKey: string;
|
||||
channelId: string;
|
||||
threadId: string;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const location = `new-thread-${props.threadId || props.channelId}`;
|
||||
const subLocation = useContext(ViewContext).type;
|
||||
const { active: editorIsActive, set: setVisibleEditor } = useVisibleMessagesEditorLocation(
|
||||
location,
|
||||
subLocation,
|
||||
);
|
||||
|
||||
if (!editorIsActive && props.useButton) {
|
||||
return (
|
||||
<Thread withBlock className="new-thread-button">
|
||||
<ThreadSection
|
||||
noSenderSpace
|
||||
onClick={() => {
|
||||
setVisibleEditor({ location, subLocation });
|
||||
}}
|
||||
>
|
||||
<PlusCircle size={16} className="plus-icon" />{' '}
|
||||
{Languages.t('scenes.apps.messages.new_thread', [], 'Start a new discussion')}
|
||||
</ThreadSection>
|
||||
</Thread>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Thread withBlock className="new-thread">
|
||||
<ThreadSection noSenderSpace>
|
||||
<Input channelId={props.channelId} threadId={props.threadId} />
|
||||
</ThreadSection>
|
||||
</Thread>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Card, Col, Row, Tooltip, Typography } from 'antd';
|
||||
import { FileType } from 'app/features/files/types/file';
|
||||
import '../input.scss';
|
||||
import React from 'react';
|
||||
|
||||
type PropsType = {
|
||||
files: FileType[];
|
||||
};
|
||||
|
||||
const { Text } = Typography;
|
||||
export default ({ files }: PropsType) => {
|
||||
return (
|
||||
<Row className="attached-files-container" justify="start">
|
||||
{files.map(f => (
|
||||
<Col key={f.id} className="attached-file">
|
||||
<Card size="small">
|
||||
<Tooltip placement="top" title={f?.metadata?.name || ''}>
|
||||
<Text ellipsis style={{ width: 80, verticalAlign: 'middle' }}>
|
||||
{f?.metadata?.name || ''}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { useEphemeralMessages } from 'app/features/messages/hooks/use-ephemeral-messages';
|
||||
import useRouterCompany from 'app/features/router/hooks/use-router-company';
|
||||
import { useEffect } from 'react';
|
||||
import { MessageContext } from '../../message/message-with-replies';
|
||||
import MessageContent from '../../message/parts/MessageContent';
|
||||
import ThreadSection from '../../parts/thread-section';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
workspaceId: string;
|
||||
threadId: string;
|
||||
onHasEphemeralMessage: () => void;
|
||||
onNotEphemeralMessage: () => void;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const companyId = useRouterCompany();
|
||||
const workspaceId = props.workspaceId;
|
||||
const channelId = props.channelId;
|
||||
const { lastEphemeral } = useEphemeralMessages({
|
||||
companyId,
|
||||
channelId: props.channelId,
|
||||
});
|
||||
|
||||
const messageKey = {
|
||||
id: lastEphemeral?.id || '',
|
||||
threadId: lastEphemeral?.thread_id || '',
|
||||
companyId,
|
||||
};
|
||||
|
||||
const hasEphemeral =
|
||||
lastEphemeral &&
|
||||
(lastEphemeral.thread_id === props.threadId ||
|
||||
(!props.threadId &&
|
||||
(!lastEphemeral.thread_id || lastEphemeral.thread_id === lastEphemeral.id)));
|
||||
|
||||
useEffect(() => {
|
||||
if (lastEphemeral && hasEphemeral) {
|
||||
props.onHasEphemeralMessage();
|
||||
} else {
|
||||
props.onNotEphemeralMessage();
|
||||
}
|
||||
}, [lastEphemeral, hasEphemeral]);
|
||||
|
||||
if (!hasEphemeral) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const updatedKey =
|
||||
lastEphemeral.id + lastEphemeral.ephemeral?.version + lastEphemeral.ephemeral?.id;
|
||||
|
||||
return (
|
||||
<div className="ephemerals" key={updatedKey}>
|
||||
<div className="ephemerals_text">
|
||||
{Languages.t('scenes.apps.messages.just_you', [], 'Visible uniquement par vous')}
|
||||
</div>
|
||||
|
||||
<MessageContext.Provider value={{ ...messageKey, companyId, workspaceId, channelId }}>
|
||||
<ThreadSection withAvatar head>
|
||||
<MessageContent key={updatedKey} />
|
||||
</ThreadSection>
|
||||
</MessageContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,276 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useState } from 'react';
|
||||
import { EditorState } from 'draft-js';
|
||||
import { Smile, Video, MoreHorizontal, Paperclip, Type } from 'react-feather';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import EmojiPicker from 'components/emoji-picker/emoji-picker';
|
||||
import Menu from 'components/menus/menu';
|
||||
import MenusManager from 'app/components/menus/menus-manager';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps';
|
||||
import MessageEditorsManager from 'app/features/messages/services/message-editor-service-factory';
|
||||
import EditorToolbar from 'app/components/rich-text-editor/editor-toolbar';
|
||||
import { Application } from 'app/features/applications/types/application';
|
||||
import { useCompanyApplications } from 'app/features/applications/hooks/use-company-applications';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
threadId: string;
|
||||
onAddEmoji?: (emoji: any) => void;
|
||||
onSend?: () => void;
|
||||
triggerApp?: (app: any, fromIcon: any, evt: any) => void;
|
||||
isEmpty: boolean;
|
||||
onRichTextChange: (editorState: EditorState) => void;
|
||||
richTextEditorState: EditorState;
|
||||
};
|
||||
|
||||
type MenuItem = {
|
||||
type: string;
|
||||
emoji?: any;
|
||||
icon?: any;
|
||||
text?: string;
|
||||
onClick?: (event: Event) => void;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const [displayRichTextOptions, setDisplayRichTextOptions] = useState(false);
|
||||
const [, setDisplayFileMenu] = useState(false);
|
||||
const [, setDisplayEmojiMenu] = useState(false);
|
||||
const addon_menu: MenuItem[] = [];
|
||||
const addon_right_icon: any[] = [];
|
||||
const addon_files: any[] = [];
|
||||
const addon_calls: any[] = [];
|
||||
|
||||
const apps = useCompanyApplications().applications.filter(
|
||||
(app: Application) => app.display?.twake?.chat?.input,
|
||||
);
|
||||
|
||||
if (props.triggerApp) {
|
||||
if (apps.length > 0) {
|
||||
// eslint-disable-next-line array-callback-return
|
||||
apps.map((app: Application) => {
|
||||
if (app) {
|
||||
let icon = WorkspacesApps.getAppIcon(app);
|
||||
let emoji = '';
|
||||
if ((icon || '').indexOf('http') === 0) {
|
||||
emoji = icon;
|
||||
icon = '';
|
||||
}
|
||||
const menu_item: MenuItem = {
|
||||
type: 'menu',
|
||||
emoji,
|
||||
icon,
|
||||
text: app.identity?.name,
|
||||
onClick: (evt: unknown) => {
|
||||
props.triggerApp && props.triggerApp(app, undefined, evt);
|
||||
},
|
||||
};
|
||||
|
||||
if (
|
||||
app.display?.twake?.chat?.input?.type === 'file' &&
|
||||
app?.identity?.code !== 'twake_drive'
|
||||
) {
|
||||
addon_files.push(menu_item);
|
||||
} else if (
|
||||
app?.identity?.code === 'jitsi' ||
|
||||
app.display?.twake?.chat?.input?.type === 'call'
|
||||
) {
|
||||
addon_calls.push(menu_item);
|
||||
} else if (app?.identity?.code !== 'twake_drive') {
|
||||
addon_menu.push(menu_item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const RichTextToolbar = () => (
|
||||
<EditorToolbar
|
||||
editorState={props.richTextEditorState}
|
||||
onChange={editorState => props.onRichTextChange(editorState)}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayToolbar = () => {
|
||||
return displayRichTextOptions;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="input-toolbar">
|
||||
<div className="input-options">
|
||||
{addon_files.length > 0 && (
|
||||
<Menu
|
||||
className="option"
|
||||
position="top"
|
||||
toggle={true}
|
||||
onOpen={() => setDisplayFileMenu(true)}
|
||||
onClose={() => setDisplayFileMenu(false)}
|
||||
menu={[
|
||||
{
|
||||
type: 'menu',
|
||||
icon: 'desktop',
|
||||
text: Languages.t('scenes.apps.messages.input.attach_file.from_computer'),
|
||||
onClick: () => {
|
||||
MessageEditorsManager.get(props.channelId).openFileSelector(props.threadId);
|
||||
},
|
||||
},
|
||||
...addon_files,
|
||||
]}
|
||||
>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={Languages.t('scenes.apps.messages.input.attach_file', [], 'Attach file(s)')}
|
||||
>
|
||||
<Button type="text" size="small" className="ant-btn-icon-only">
|
||||
<Paperclip size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{addon_files.length === 0 && (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={Languages.t('scenes.apps.messages.input.attach_file', [], 'Attach file(s)')}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="ant-btn-icon-only"
|
||||
onClick={() =>
|
||||
MessageEditorsManager.get(props.channelId).openFileSelector(props.threadId)
|
||||
}
|
||||
>
|
||||
<Paperclip size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{props.onAddEmoji && (
|
||||
<Menu
|
||||
className="option"
|
||||
position="top"
|
||||
toggle={true}
|
||||
onOpen={() => setDisplayEmojiMenu(true)}
|
||||
onClose={() => setDisplayEmojiMenu(false)}
|
||||
menu={[
|
||||
{
|
||||
type: 'react-element',
|
||||
className: 'menu-cancel-margin',
|
||||
reactElement: () => {
|
||||
return (
|
||||
<EmojiPicker
|
||||
onChange={(emoji: unknown) => {
|
||||
MenusManager.closeMenu();
|
||||
props.onAddEmoji && props.onAddEmoji(emoji);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={Languages.t('scenes.apps.messages.input.emoji', [], 'Emoji')}
|
||||
>
|
||||
<Button type="text" size="small">
|
||||
<Smile size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{addon_calls.length > 1 && (
|
||||
<Menu className="option" position="top" menu={addon_calls}>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={Languages.t('scenes.apps.messages.input.start_call', [], 'Start a call')}
|
||||
>
|
||||
<Button type="text" size="small" className="ant-btn-icon-only">
|
||||
<Video size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{addon_calls.length === 1 && (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={Languages.t('scenes.apps.messages.input.start_call', [], 'Start a call')}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="ant-btn-icon-only option"
|
||||
onClick={evt => addon_calls[0].onClick(evt)}
|
||||
>
|
||||
<Video size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={
|
||||
displayRichTextOptions
|
||||
? Languages.t('scenes.apps.messages.input.hide_formatting', [], 'Hide formatting')
|
||||
: Languages.t('scenes.apps.messages.input.show_formatting', [], 'Show formatting')
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className={`option ant-btn-icon-only richtext ${
|
||||
displayRichTextOptions ? 'selected' : ''
|
||||
}`}
|
||||
onMouseDown={e => {
|
||||
e.preventDefault();
|
||||
setDisplayRichTextOptions(!displayRichTextOptions);
|
||||
}}
|
||||
>
|
||||
<Type size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{addon_right_icon.map((app: Application) => {
|
||||
return (
|
||||
<Button
|
||||
key={app.id}
|
||||
type="text"
|
||||
size="small"
|
||||
className="option"
|
||||
onClick={(evt: unknown) => {
|
||||
props.triggerApp && props.triggerApp(app, true, evt);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="messages-input-app-icon"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url(' + (app.display?.twake?.chat?.input?.icon || app.identity?.icon) + ')',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
||||
{addon_menu.length > 0 && (
|
||||
<Menu className="option" position="top" menu={addon_menu}>
|
||||
<Button type="text" size="small">
|
||||
<MoreHorizontal size={16} />
|
||||
</Button>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{displayToolbar() && (
|
||||
<div className="input-options-toolbar">
|
||||
<div className="richtext-toolbar fade_in">
|
||||
<RichTextToolbar />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useChannelWritingActivityState } from 'app/features/channels/hooks/use-channel-writing-activity';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import WritingLoader from 'app/components/writing-loader/writing-loader';
|
||||
import { ChannelWritingActivityType } from 'app/features/channels/state/channel-writing-activity';
|
||||
|
||||
type PropsType = {
|
||||
channelId: string;
|
||||
threadId: string;
|
||||
};
|
||||
|
||||
const getChannelActivityMessage = (channelActivityUsers: ChannelWritingActivityType[]) => {
|
||||
const channelActivityUsersCount = channelActivityUsers.length;
|
||||
let channelActivityMessage = '';
|
||||
|
||||
if (channelActivityUsersCount === 1) {
|
||||
channelActivityMessage = Languages.t(
|
||||
'scenes.app.messages.input.parts.is_writing.message.user_is_writing',
|
||||
[channelActivityUsers[0].name],
|
||||
);
|
||||
}
|
||||
|
||||
if (channelActivityUsersCount === 2) {
|
||||
channelActivityMessage = Languages.t(
|
||||
'scenes.app.messages.input.parts.is_writing.message.users_are_writing',
|
||||
[channelActivityUsers[0].name, channelActivityUsers[1].name],
|
||||
);
|
||||
}
|
||||
|
||||
if (channelActivityUsersCount > 2) {
|
||||
channelActivityMessage = Languages.t(
|
||||
'scenes.app.messages.input.parts.is_writing.message.users_and_more_are_writing',
|
||||
[channelActivityUsers[0].name, channelActivityUsers[1].name, channelActivityUsersCount - 2],
|
||||
);
|
||||
}
|
||||
|
||||
return channelActivityMessage;
|
||||
};
|
||||
|
||||
export default ({ channelId, threadId }: PropsType): JSX.Element => {
|
||||
const channelActivityUsers = useChannelWritingActivityState(channelId, threadId);
|
||||
const [writtingInfo, setWritingInfo] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const message = getChannelActivityMessage(channelActivityUsers);
|
||||
|
||||
setWritingInfo(message);
|
||||
}, [channelActivityUsers]);
|
||||
|
||||
return writtingInfo.length > 0 ? (
|
||||
<div className="user-writing-info-message-view">
|
||||
<div className="user-writing-info-message-view-inner">
|
||||
<WritingLoader /> <div className="small-left-margin">{writtingInfo}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { MessageFileType } from 'app/features/messages/types/message';
|
||||
import { useUploadZones } from 'app/features/files/hooks/use-upload-zones';
|
||||
import { ChannelType } from 'app/features/channels/types/channel';
|
||||
import { useMessageEditor } from 'app/features/messages/hooks/use-message-editor';
|
||||
|
||||
export default (props: {
|
||||
children: ReactNode;
|
||||
setHandler: (handler: (file: MessageFileType) => void) => void;
|
||||
channel: ChannelType;
|
||||
threadId?: string;
|
||||
}) => {
|
||||
const { key: editorId } = useMessageEditor({
|
||||
companyId: props.channel.company_id || '',
|
||||
workspaceId: props.channel.workspace_id || '',
|
||||
channelId: props.channel.id,
|
||||
threadId: props.threadId,
|
||||
});
|
||||
const { files, setFiles } = useUploadZones(editorId);
|
||||
|
||||
useEffect(() => {
|
||||
props.setHandler((file: MessageFileType) => {
|
||||
setFiles([...files, file]);
|
||||
});
|
||||
}, [props.setHandler]);
|
||||
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Col, Row } from 'antd';
|
||||
import { useUploadZones } from 'app/features/files/hooks/use-upload-zones';
|
||||
import '../input.scss';
|
||||
import { MessageFileType } from 'app/features/messages/types/message';
|
||||
import _ from 'lodash';
|
||||
import PossiblyPendingAttachment from '../../message/parts/PossiblyPendingAttachment';
|
||||
|
||||
type PropsType = {
|
||||
zoneId: string;
|
||||
initialValue?: MessageFileType[];
|
||||
onChange?: (list: MessageFileType[]) => void;
|
||||
};
|
||||
|
||||
export default ({ zoneId, onChange, initialValue }: PropsType) => {
|
||||
const { files, setFiles } = useUploadZones(zoneId);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValue) setFiles(initialValue.filter(f => f?.metadata?.source !== 'pending'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) onChange(files);
|
||||
}, [files]);
|
||||
|
||||
return files.length > 0 ? (
|
||||
<Row className="attached-files-container mt-2 -mb-1" justify="start">
|
||||
{files.map((file, index) => {
|
||||
return (
|
||||
<Col key={index}>
|
||||
<PossiblyPendingAttachment
|
||||
file={file}
|
||||
type={'input'}
|
||||
onRemove={() =>
|
||||
setFiles(
|
||||
files.filter(
|
||||
f => !_.isEqual(f.metadata?.external_id, file.metadata?.external_id),
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
@@ -1,150 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, {
|
||||
forwardRef,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ItemContent, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import _ from 'lodash';
|
||||
|
||||
export type ListBuilderHandle = VirtuosoHandle & unknown;
|
||||
|
||||
type Props = {
|
||||
items: any[];
|
||||
followOutput: false | 'smooth' | 'auto';
|
||||
loadMore: (direction: 'history' | 'future', limit: number, offset?: any) => Promise<unknown[]>;
|
||||
itemContent: ItemContent<any, any>;
|
||||
itemId: (item: any) => string;
|
||||
emptyListComponent: ReactNode;
|
||||
onScroll: (e: React.UIEvent<'div', UIEvent>) => void;
|
||||
style?: React.CSSProperties;
|
||||
atBottomStateChange?: (atBottom: boolean) => void;
|
||||
//Will be called just before to finish append messages for a final filtering
|
||||
filterOnAppend?: (item: any[]) => any[];
|
||||
};
|
||||
|
||||
let prependMoreLock = false;
|
||||
let appendMoreLock = false;
|
||||
|
||||
export default React.memo(
|
||||
forwardRef(
|
||||
(
|
||||
{
|
||||
emptyListComponent,
|
||||
filterOnAppend,
|
||||
followOutput,
|
||||
itemId,
|
||||
loadMore,
|
||||
onScroll,
|
||||
items: _items,
|
||||
itemContent,
|
||||
atBottomStateChange,
|
||||
style,
|
||||
}: Props,
|
||||
ref,
|
||||
) => {
|
||||
const START_INDEX = 10000000;
|
||||
const INITIAL_ITEM_COUNT = (_items || []).length;
|
||||
|
||||
const [firstItemIndex, setFirstItemIndex] = useState(START_INDEX);
|
||||
const [items, setItems] = useState(_items || []);
|
||||
const refVirtuoso = useRef<VirtuosoHandle>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
...refVirtuoso.current,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
// Detect append or prepend or full replace items
|
||||
const ids = items.map(i => itemId(i));
|
||||
//Find the first index in props _items that is already in displayed items
|
||||
const first = _items.findIndex(i => ids.includes(itemId(i)));
|
||||
//Find the last index in props _items that is already in displayed items
|
||||
const lastIndex = _items
|
||||
.slice()
|
||||
.reverse()
|
||||
.findIndex(i => ids.includes(itemId(i)));
|
||||
const last = lastIndex >= 0 ? _items.length - 1 - lastIndex : lastIndex;
|
||||
if (first == -1) {
|
||||
//Replacement
|
||||
setFirstItemIndex(START_INDEX);
|
||||
setItems(_items);
|
||||
} else if (first === 0 && last !== _items.length - 1) {
|
||||
//Append
|
||||
let newList = [...items, ..._items.slice(last + 1)];
|
||||
if (filterOnAppend) newList = filterOnAppend(newList);
|
||||
setItems(newList);
|
||||
} else if (last === _items.length - 1 && first !== 0) {
|
||||
//Prepend
|
||||
const newItems = _items.slice(0, first);
|
||||
const nextFirstItemIndex = firstItemIndex - newItems.length;
|
||||
setFirstItemIndex(() => nextFirstItemIndex);
|
||||
setItems([...newItems, ...items]);
|
||||
} else {
|
||||
if (filterOnAppend) {
|
||||
const newList = filterOnAppend([...items, ..._items]);
|
||||
if (
|
||||
_.difference(
|
||||
items.map(i => itemId(i)),
|
||||
newList.map(i => itemId(i)),
|
||||
).length > 0
|
||||
) {
|
||||
setItems(newList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [_items]);
|
||||
|
||||
const appendItems = useCallback(() => {
|
||||
if (appendMoreLock) return;
|
||||
appendMoreLock = true;
|
||||
|
||||
setTimeout(async () => {
|
||||
await loadMore('future', 20, items[items.length - 1]);
|
||||
appendMoreLock = false;
|
||||
}, 10);
|
||||
|
||||
return false;
|
||||
}, [items]);
|
||||
|
||||
const prependItems = useCallback(() => {
|
||||
if (prependMoreLock) return;
|
||||
prependMoreLock = true;
|
||||
|
||||
setTimeout(async () => {
|
||||
await loadMore('history', 20, items[0]);
|
||||
prependMoreLock = false;
|
||||
}, 10);
|
||||
|
||||
return false;
|
||||
}, [items]);
|
||||
|
||||
if (items.length === 0) {
|
||||
return <div style={{ flex: 1 }}>{emptyListComponent}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Virtuoso
|
||||
ref={refVirtuoso}
|
||||
style={style}
|
||||
followOutput={followOutput}
|
||||
alignToBottom={true}
|
||||
firstItemIndex={firstItemIndex}
|
||||
initialTopMostItemIndex={INITIAL_ITEM_COUNT - 1}
|
||||
data={items}
|
||||
startReached={prependItems}
|
||||
endReached={appendItems}
|
||||
itemContent={itemContent}
|
||||
onScroll={e => onScroll(e)}
|
||||
atBottomStateChange={atBottomStateChange}
|
||||
computeItemKey={(_index, item) => itemId(item)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -1,92 +0,0 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import Message from './message';
|
||||
import Responses from './responses';
|
||||
import ReplyBlock from './parts/ReplyBlock';
|
||||
import LoadMoreReplies from './parts/LoadMoreReplies';
|
||||
import { MessagesListContext } from '../messages-list';
|
||||
import ThreadSection from '../parts/thread-section';
|
||||
import Thread from '../parts/thread';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import ActivityMessage, { ActivityType } from './parts/ChannelActivity/ActivityMessage';
|
||||
import { useHighlightMessage } from 'app/features/messages/hooks/use-highlight-message';
|
||||
import { NodeMessage } from 'app/features/messages/types/message';
|
||||
|
||||
export const MessageContext = React.createContext({
|
||||
companyId: '',
|
||||
workspaceId: '',
|
||||
channelId: '',
|
||||
threadId: '',
|
||||
id: '',
|
||||
});
|
||||
|
||||
type Props = {
|
||||
companyId: string;
|
||||
workspaceId: string;
|
||||
channelId: string;
|
||||
threadId: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export default React.memo(({ threadId, workspaceId, channelId, companyId, id }: Props) => {
|
||||
return (
|
||||
<MessageContext.Provider
|
||||
value={{ companyId, workspaceId, channelId, threadId, id: id || threadId }}
|
||||
>
|
||||
<MessageType />
|
||||
</MessageContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
const MessageType = () => {
|
||||
const listContext = useContext(MessagesListContext);
|
||||
const context = useContext(MessageContext);
|
||||
const { message } = useMessage(context);
|
||||
const { highlight } = useHighlightMessage();
|
||||
const highlighted =
|
||||
(highlight &&
|
||||
((highlight.threadId === context.id && !listContext.hideReplies) ||
|
||||
highlight.answerId === context.id)) ||
|
||||
false;
|
||||
const [firstMessageId, setFirstMessageId] = useState(
|
||||
message.last_replies?.[0]?.id || message.thread_id,
|
||||
);
|
||||
|
||||
if (message.subtype === 'system') {
|
||||
const activity = message?.context?.activity as ActivityType;
|
||||
return <ActivityMessage message={message} activity={activity} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Thread withBlock={listContext.withBlock} highlighted={highlighted}>
|
||||
<HeadMessage />
|
||||
{!listContext.hideReplies && (
|
||||
<>
|
||||
<LoadMoreReplies
|
||||
firstMessageId={firstMessageId}
|
||||
onFirstMessageChanged={(firstMessage: NodeMessage) => {
|
||||
if (firstMessage) setFirstMessageId(firstMessage.id);
|
||||
}}
|
||||
/>
|
||||
<Responses
|
||||
companyId={context.companyId}
|
||||
workspaceId={context.workspaceId}
|
||||
channelId={context.channelId}
|
||||
threadId={context.threadId}
|
||||
firstMessageId={firstMessageId}
|
||||
/>
|
||||
{!listContext.readonly && <ReplyBlock />}
|
||||
</>
|
||||
)}
|
||||
</Thread>
|
||||
);
|
||||
};
|
||||
|
||||
const HeadMessage = () => {
|
||||
const context = useContext(MessageContext);
|
||||
const { message } = useMessage(context);
|
||||
return (
|
||||
<ThreadSection withAvatar head pinned={!!message.pinned_info?.pinned_at}>
|
||||
<Message />
|
||||
</ThreadSection>
|
||||
);
|
||||
};
|
||||
@@ -1,245 +0,0 @@
|
||||
.message {
|
||||
text-align: left;
|
||||
.deleted-message {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.message_is_loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.message-not-sent.content {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.alert_failed_icon {
|
||||
margin-left: 8px;
|
||||
margin-top: 0px;
|
||||
display: flex;
|
||||
color: var(--red);
|
||||
}
|
||||
.sender-space {
|
||||
width: 28px;
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
.sender-head,
|
||||
.app_icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--grey-background);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
.online_user_status {
|
||||
position: relative;
|
||||
bottom: -20px;
|
||||
right: -20px;
|
||||
}
|
||||
}
|
||||
|
||||
.sender-head {
|
||||
.user_online_stPPatus {
|
||||
right: -20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.message-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: 0;
|
||||
|
||||
&.link-to-thread {
|
||||
color: var(--grey-dark);
|
||||
.content-parent {
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
border-radius: 2px;
|
||||
background: var(--grey-light);
|
||||
}
|
||||
position: relative;
|
||||
padding-left: 8px;
|
||||
}
|
||||
.sender-status {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.message-content-header-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
.message-content-header {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
|
||||
.sender-name {
|
||||
font-weight: 700;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sender-status {
|
||||
font-size: 12px;
|
||||
color: var(--grey-dark);
|
||||
margin-right: 4px;
|
||||
}
|
||||
.reply-text {
|
||||
margin-right: 4px;
|
||||
a,
|
||||
a * {
|
||||
display: inline;
|
||||
}
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.date {
|
||||
color: var(--grey-dark);
|
||||
font-size: 12px;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.message_header_loader {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-options {
|
||||
position: absolute;
|
||||
background-color: var(--white);
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--grey-light);
|
||||
display: block;
|
||||
top: -16px;
|
||||
margin: auto;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
|
||||
&.drag {
|
||||
left: -8px;
|
||||
right: unset;
|
||||
display: none;
|
||||
|
||||
.option,
|
||||
.option:hover {
|
||||
min-width: 28px;
|
||||
color: var(--primary);
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--grey-light);
|
||||
}
|
||||
|
||||
.option {
|
||||
min-width: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--grey-dark);
|
||||
cursor: pointer;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
background-color: var(--primary-background);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border: 1px solid var(--primary);
|
||||
background-color: var(--primary-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
.message-content.active {
|
||||
.message-options.right {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.message-options {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content.loading-interaction {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.reactions {
|
||||
padding-top: 8px;
|
||||
|
||||
.reaction_container {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.reaction {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 4px;
|
||||
border-radius: var(--border-radius-base);
|
||||
box-sizing: border-box;
|
||||
color: var(--black);
|
||||
height: 24px;
|
||||
min-width: 36px;
|
||||
border: 0;
|
||||
padding: 3px 4px;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
border: 1px solid var(--grey-background);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
|
||||
.emoji-container {
|
||||
margin-right: 4px;
|
||||
line-height: 0px;
|
||||
}
|
||||
|
||||
&.is_selected {
|
||||
border: 1px solid var(--primary);
|
||||
color: var(--primary);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body:not(.drag_message) {
|
||||
.draggable {
|
||||
.message-options.drag {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import MessageContent from './parts/MessageContent';
|
||||
|
||||
export default () => {
|
||||
return <MessageContent />;
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
import Twacode from 'app/components/twacode/twacode';
|
||||
import React, { ReactNode, Suspense } from 'react';
|
||||
import Markdown from 'markdown-to-jsx';
|
||||
import HighlightedCode from 'app/components/highlighted-code/highlighted-code';
|
||||
import { preparse, preunparse } from './Blocks.utils';
|
||||
import User from 'components/twacode/blocks/user';
|
||||
import Chan from 'components/twacode/blocks/chan';
|
||||
import { blocksToTwacode, formatData } from 'app/components/twacode/blocksCompiler';
|
||||
import environment from 'app/environment/environment';
|
||||
import { Block } from 'app/components/twacode/types';
|
||||
|
||||
type Props = {
|
||||
blocks: unknown[];
|
||||
fallback: string | ReactNode;
|
||||
onAction: (type: string, id: string, context: unknown, passives: unknown, evt: unknown) => void;
|
||||
allowAdvancedBlocks?: boolean;
|
||||
};
|
||||
|
||||
const Code = ({ className, children }: { className: string; children: string }) => {
|
||||
children = preunparse(children);
|
||||
if (children.split('\n').length === 1) {
|
||||
return <code>{children}</code>;
|
||||
}
|
||||
return <HighlightedCode className={className + ' multiline-code'} code={children} />;
|
||||
};
|
||||
|
||||
const Link = ({ href, children }: { href: string; children: string }) => {
|
||||
let target = '_blank';
|
||||
if (!href) {
|
||||
return <a href="#">{children}</a>;
|
||||
}
|
||||
//If same domain, stay on the same tab
|
||||
if (
|
||||
href
|
||||
?.replace(/https?:\/\//g, '')
|
||||
?.split('/')[0]
|
||||
?.toLocaleLowerCase() ===
|
||||
environment.front_root_url
|
||||
.replace(/https?:\/\//g, '')
|
||||
.split('/')[0]
|
||||
?.toLocaleLowerCase()
|
||||
) {
|
||||
target = '_self';
|
||||
}
|
||||
|
||||
return (
|
||||
<a target={target} rel="noreferrer" href={href?.replace(/^javascript:/, '')}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo((props: Props) => {
|
||||
const flattedBlocks: Block[] = [];
|
||||
formatData(props.blocks || [], 'content', flattedBlocks);
|
||||
const blocks = blocksToTwacode(flattedBlocks);
|
||||
|
||||
if (!props.blocks?.length || !props.allowAdvancedBlocks) {
|
||||
return typeof props.fallback === 'string' ? (
|
||||
<div className="markdown">
|
||||
<Markdown
|
||||
options={{
|
||||
forceBlock: true,
|
||||
overrides: {
|
||||
code: {
|
||||
component: Code,
|
||||
},
|
||||
a: {
|
||||
component: Link,
|
||||
},
|
||||
user: {
|
||||
component: ({ id }) => (
|
||||
<>
|
||||
<User hideUserImage={false} username={id} />{' '}
|
||||
</>
|
||||
),
|
||||
},
|
||||
channel: {
|
||||
component: ({ id }) => (
|
||||
<>
|
||||
<Chan id={id} name={id} />{' '}
|
||||
</>
|
||||
),
|
||||
},
|
||||
h1: ({ children }) => children,
|
||||
h2: ({ children }) => children,
|
||||
h3: ({ children }) => children,
|
||||
h4: ({ children }) => children,
|
||||
h5: ({ children }) => children,
|
||||
h6: ({ children }) => children,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{preparse(props.fallback || '')}
|
||||
</Markdown>
|
||||
</div>
|
||||
) : (
|
||||
<>{props.fallback}</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<></>}>
|
||||
<Twacode
|
||||
content={blocks}
|
||||
isApp={props.allowAdvancedBlocks}
|
||||
onAction={(type: string, id: string, context: unknown, passives: unknown, evt: unknown) =>
|
||||
props.onAction(type, id, context, passives, evt)
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
export const preparse = (str: string) => {
|
||||
const ret = str
|
||||
//Fix markdown simple line break
|
||||
.replace(/\n/g, ' \n')
|
||||
.replace(/^(([0-9]+\.)|[>`\-*].*\n) *$\n/gm, '$1\n\n')
|
||||
//Prevent html security issues
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
//Prepare mentions
|
||||
.replace(/([ \t]|^)@([\w]+)/g, '$1<user id="$2"/>')
|
||||
.replace(/([ \t]|^)#([\w]+)/g, '$1<channel id="$2"/>')
|
||||
//Prepare code
|
||||
.replace(/[^\n]\r?\n{1}`{3}/, '\n\n```');
|
||||
return ret;
|
||||
};
|
||||
|
||||
export const preunparse = (str: string) => {
|
||||
return (
|
||||
str
|
||||
//Prepare mentions
|
||||
.replace(/([ \t]|^)<user id="([\w]+)"\/>/g, '$1@$2')
|
||||
.replace(/([ \t]|^)<channel id="([\w]+)"\/>/g, '$1#$2')
|
||||
//Prevent html security issues
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
);
|
||||
};
|
||||
-275
@@ -1,275 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Row, Typography } from 'antd';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import Emojione from 'app/components/emojione/emojione';
|
||||
import User from 'app/components/twacode/blocks/user';
|
||||
import { ChannelType } from 'app/features/channels/types/channel';
|
||||
import { TabType } from 'app/features/tabs/types/tab';
|
||||
import { getCompanyApplications } from 'app/features/applications/state/company-applications';
|
||||
import Groups from 'app/deprecated/workspaces/groups.js';
|
||||
import { ChannelMemberType } from 'app/features/channel-members/types/channel-member-types';
|
||||
import { MessageWithReplies } from 'app/features/messages/types/message';
|
||||
import { Application } from 'app/features/applications/types/application';
|
||||
|
||||
enum ChannelActivityEnum {
|
||||
CHANNEL_MEMBER_CREATED = 'channel:activity:member:created',
|
||||
CHANNEL_MEMBER_DELETED = 'channel:activity:member:deleted',
|
||||
CHANNEL_UPDATED = 'channel:activity:updated',
|
||||
CHANNEL_TAB_CREATED = 'channel:activity:tab:created',
|
||||
CHANNEL_TAB_DELETED = 'channel:activity:tab:deleted',
|
||||
CHANNEL_CONNECTOR_CREATED = 'channel:activity:connector:created',
|
||||
CHANNEL_CONNECTOR_DELETED = 'channel:activity:connector:deleted',
|
||||
}
|
||||
|
||||
export type ActivityType = {
|
||||
type: ChannelActivityEnum;
|
||||
actor: {
|
||||
type: 'user';
|
||||
id: string;
|
||||
};
|
||||
context: {
|
||||
type: 'add' | 'diff' | 'remove';
|
||||
array?: {
|
||||
type: string;
|
||||
resource: ChannelMemberType | TabType;
|
||||
}[];
|
||||
previous?: {
|
||||
type: string;
|
||||
// should be a real type instead
|
||||
resource: { id: string; name: string; description: string; icon: string };
|
||||
};
|
||||
next?: {
|
||||
type: string;
|
||||
// should be a real type instead
|
||||
resource: { id: string; name: string; description: string; icon: string };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
refDom?: React.Ref<HTMLDivElement>;
|
||||
activity: ActivityType;
|
||||
message: MessageWithReplies;
|
||||
};
|
||||
|
||||
// i18n but with react nodes as replacements
|
||||
// TODO: maybe there is betters ways to do it with lodash
|
||||
const translateUsingReactNode = (key: string, replacements: JSX.Element[]): JSX.Element[] => {
|
||||
let temp =
|
||||
Languages.t(
|
||||
key,
|
||||
replacements.map((_, i) => `{${i}}`),
|
||||
) || '';
|
||||
const list: JSX.Element[] = [];
|
||||
replacements.forEach((replacement, i) => {
|
||||
const split = temp.split(`{${i}}`);
|
||||
list.push(
|
||||
<Typography.Text key={i} type="secondary">
|
||||
{split[0]}
|
||||
</Typography.Text>,
|
||||
);
|
||||
list.push(replacement);
|
||||
temp = split[1];
|
||||
});
|
||||
list.push(
|
||||
<Typography.Text key={key + 'b'} type="secondary">
|
||||
{temp}
|
||||
</Typography.Text>,
|
||||
);
|
||||
return list;
|
||||
};
|
||||
|
||||
export default (props: PropsType): JSX.Element => {
|
||||
const generateTypographyName = (id: string) => <User id={id} username="Unknown" hideUserImage />;
|
||||
const message = props.message;
|
||||
|
||||
const memberJoinedOrInvited = (activity: ActivityType) => {
|
||||
if (activity.context.array) {
|
||||
const resource = activity.context.array[0]?.resource as ChannelMemberType;
|
||||
|
||||
if (activity.actor.id === resource.user_id) {
|
||||
return [<></>]; //Do not show this information to not polute the chat
|
||||
}
|
||||
|
||||
if (activity.actor.id !== resource.user_id) {
|
||||
return translateUsingReactNode(
|
||||
'scenes.apps.messages.message.activity_message.a_added_b_to_the_channel',
|
||||
[
|
||||
<span key={message.id + '-1'} style={{ marginRight: 5, lineHeight: 0 }}>
|
||||
{generateTypographyName(activity.actor.id)}
|
||||
</span>,
|
||||
<span key={message.id + '-2'} style={{ margin: '0 5px', lineHeight: 0 }}>
|
||||
{generateTypographyName(resource.user_id || '')}
|
||||
</span>,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const memberLeftOrRemoved = (activity: ActivityType) => {
|
||||
if (activity.context.array) {
|
||||
const resource = activity.context.array[0]?.resource as ChannelMemberType;
|
||||
if (activity.actor.id === resource.user_id) {
|
||||
return []; //Do not show this information to not polute the chat
|
||||
}
|
||||
|
||||
if (activity.actor.id !== resource.user_id) {
|
||||
return translateUsingReactNode(
|
||||
'scenes.apps.messages.message.activity_message.a_removed_b_from_the_channel',
|
||||
[
|
||||
<span key={message.id + '-3'} style={{ marginRight: 5, lineHeight: 0 }}>
|
||||
{generateTypographyName(activity.actor.id)}
|
||||
</span>,
|
||||
<span key={message.id + '-4'} style={{ margin: '0 5px', lineHeight: 0 }}>
|
||||
{generateTypographyName(resource.user_id || '')}
|
||||
</span>,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const channelNameOrDescription = (activity: ActivityType) => {
|
||||
const previous = activity.context.previous;
|
||||
const next = activity.context.next;
|
||||
|
||||
if (
|
||||
previous?.resource?.name !== next?.resource?.name ||
|
||||
previous?.resource?.icon !== next?.resource?.icon
|
||||
) {
|
||||
const icon = <Emojione type={next?.resource.icon || ''} />;
|
||||
|
||||
return translateUsingReactNode(
|
||||
'scenes.apps.messages.message.activity_message.a_updated_channel_name',
|
||||
[
|
||||
<span key={message.id + '-5'} style={{ marginRight: 5, lineHeight: 0 }}>
|
||||
{generateTypographyName(activity.actor.id)}
|
||||
</span>,
|
||||
<Typography.Text key={message.id + '-6'} strong style={{ margin: '0 5px' }}>
|
||||
{icon} {next?.resource?.name}
|
||||
</Typography.Text>,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (previous?.resource.description !== next?.resource.description) {
|
||||
return translateUsingReactNode(
|
||||
'scenes.apps.messages.message.activity_message.a_updated_channel_description',
|
||||
[
|
||||
<span key={message.id + '-7'} style={{ marginRight: 5, lineHeight: 0 }}>
|
||||
{generateTypographyName(activity.actor.id)}
|
||||
</span>,
|
||||
],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const channelTabCreatedOrDeleted = (activity: ActivityType) => {
|
||||
if (activity.context.array) {
|
||||
const resource = activity.context.array[0].resource as TabType;
|
||||
const connector = getCompanyApplications(Groups.currentGroupId).filter(
|
||||
(app: Application) => app.id === resource.application_id,
|
||||
);
|
||||
|
||||
if (activity.context.type === 'add') {
|
||||
return translateUsingReactNode(
|
||||
'scenes.apps.messages.message.activity_message.a_created_channel_tab',
|
||||
[
|
||||
<span key={message.id + '-8'} style={{ marginRight: 5, lineHeight: 0 }}>
|
||||
{generateTypographyName(activity.actor.id)}
|
||||
</span>,
|
||||
<Typography.Text key={message.id + '-9'} strong style={{ margin: '0 5px' }}>
|
||||
{connector[0]?.identity?.name}
|
||||
</Typography.Text>,
|
||||
<Typography.Text key={message.id + '-10'} strong style={{ marginLeft: 5 }}>
|
||||
{resource?.name}
|
||||
</Typography.Text>,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (activity.context.type === 'remove') {
|
||||
return translateUsingReactNode(
|
||||
'scenes.apps.messages.message.activity_message.a_deleted_channel_tab',
|
||||
[
|
||||
<span key={message.id + '-11'} style={{ marginRight: 5, lineHeight: 0 }}>
|
||||
{generateTypographyName(activity.actor.id)}
|
||||
</span>,
|
||||
<Typography.Text key={message.id + '-12'} strong style={{ margin: '0 5px' }}>
|
||||
{connector[0]?.identity?.name}
|
||||
</Typography.Text>,
|
||||
<Typography.Text key={message.id + '-13'} strong style={{ marginLeft: 5 }}>
|
||||
{resource?.name}
|
||||
</Typography.Text>,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const channelConnectorCreatedOrDeleted = (activity: ActivityType) => {
|
||||
if (activity.context.array) {
|
||||
const resource = activity.context.array[0].resource as ChannelType;
|
||||
const connector = getCompanyApplications(Groups.currentGroupId).filter((app: Application) =>
|
||||
resource.connectors?.includes(app.id),
|
||||
);
|
||||
|
||||
if (connector.length) {
|
||||
if (activity.context.type === 'add') {
|
||||
return translateUsingReactNode(
|
||||
'scenes.apps.messages.message.activity_message.a_created_channel_connector',
|
||||
[
|
||||
<span key={message.id + '-14'} style={{ marginRight: 5, lineHeight: 0 }}>
|
||||
{generateTypographyName(activity.actor.id)}
|
||||
</span>,
|
||||
<Typography.Text key={message.id + '-15'} strong style={{ marginLeft: 5 }}>
|
||||
{connector[0]?.identity?.name}
|
||||
</Typography.Text>,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (activity.context.type === 'remove') {
|
||||
return translateUsingReactNode(
|
||||
'scenes.apps.messages.message.activity_message.a_deleted_channel_connector',
|
||||
[
|
||||
<span key={message.id + '-16'} style={{ marginRight: 5, lineHeight: 0 }}>
|
||||
{generateTypographyName(activity.actor.id)}
|
||||
</span>,
|
||||
<Typography.Text key={message.id + '-17'} strong style={{ marginLeft: 5 }}>
|
||||
{connector[0]?.identity?.name}
|
||||
</Typography.Text>,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const compute = (): string | JSX.Element[] => {
|
||||
const process = new Map<
|
||||
ChannelActivityEnum,
|
||||
(activity: ActivityType) => JSX.Element[] | undefined
|
||||
>();
|
||||
process
|
||||
.set(ChannelActivityEnum.CHANNEL_MEMBER_CREATED, memberJoinedOrInvited)
|
||||
.set(ChannelActivityEnum.CHANNEL_MEMBER_DELETED, memberLeftOrRemoved)
|
||||
.set(ChannelActivityEnum.CHANNEL_UPDATED, channelNameOrDescription)
|
||||
.set(ChannelActivityEnum.CHANNEL_TAB_CREATED, channelTabCreatedOrDeleted)
|
||||
.set(ChannelActivityEnum.CHANNEL_TAB_DELETED, channelTabCreatedOrDeleted)
|
||||
.set(ChannelActivityEnum.CHANNEL_CONNECTOR_CREATED, channelConnectorCreatedOrDeleted)
|
||||
.set(ChannelActivityEnum.CHANNEL_CONNECTOR_DELETED, channelConnectorCreatedOrDeleted);
|
||||
|
||||
const method = process.get(props.activity?.type) || (() => undefined);
|
||||
return method(props.activity) || '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: 40, paddingTop: 8 }}>
|
||||
<Row className="markdown" align="middle" justify="center" ref={props.refDom}>
|
||||
{compute()}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import UsersService from 'app/features/users/services/current-user-service';
|
||||
import userAsyncGet from 'app/features/users/utils/async-get';
|
||||
import MenuManager from 'app/components/menus/menus-manager';
|
||||
import UserCard from 'app/components/user-card/user-card';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
|
||||
import '../message.scss';
|
||||
import { Typography } from 'antd';
|
||||
import { useDirectChannels } from 'app/features/channels/hooks/use-direct-channels';
|
||||
type PropsType = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export default ({ userId }: PropsType) => {
|
||||
const [fullName, setFullName] = useState<string>('');
|
||||
const { openDiscussion } = useDirectChannels();
|
||||
|
||||
const _setFullName = async () => {
|
||||
const user = await userAsyncGet(userId);
|
||||
if (user) setFullName(UsersService.getFullName(user));
|
||||
else setFullName('not found');
|
||||
};
|
||||
useEffect(() => {
|
||||
_setFullName();
|
||||
});
|
||||
|
||||
let user_name_node: HTMLSpanElement | null = null;
|
||||
const displayUserCard = async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const box = (window as any).getBoundingClientRect(user_name_node);
|
||||
const user = await userAsyncGet(userId);
|
||||
|
||||
MenuManager.openMenu(
|
||||
[
|
||||
user && {
|
||||
type: 'react-element',
|
||||
reactElement: () => (
|
||||
<UserCard user={user} onClick={() => openDiscussion([user.id || ''])} />
|
||||
),
|
||||
},
|
||||
],
|
||||
box,
|
||||
null,
|
||||
{ margin: 8 },
|
||||
);
|
||||
};
|
||||
|
||||
const translateUsingReactNode = (key: string, replacements: JSX.Element[]): JSX.Element[] => {
|
||||
let temp = Languages.t(
|
||||
key,
|
||||
replacements.map((_, i) => `{${i}}`),
|
||||
);
|
||||
const list: JSX.Element[] = [];
|
||||
replacements.forEach((replacement, i) => {
|
||||
const split = temp.split(`{${i}}`);
|
||||
list.push(
|
||||
<Typography.Text key={i} type="secondary">
|
||||
{split[0]}
|
||||
</Typography.Text>,
|
||||
);
|
||||
list.push(replacement);
|
||||
temp = split[1];
|
||||
});
|
||||
list.push(<Typography.Text type="secondary">{temp}</Typography.Text>);
|
||||
return list;
|
||||
};
|
||||
|
||||
const clickableFullName = (
|
||||
<span
|
||||
ref={node => (user_name_node = node)}
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
marginRight: 4,
|
||||
cursor: ' pointer',
|
||||
}}
|
||||
onClick={() => displayUserCard()}
|
||||
>
|
||||
{fullName}
|
||||
</span>
|
||||
);
|
||||
|
||||
const isCurrentUser = UsersService.getCurrentUserId() === userId;
|
||||
return isCurrentUser ? (
|
||||
<>{Languages.t('scenes.apps.messages.message.parts.deleted_content.text.current_user')}</>
|
||||
) : (
|
||||
<>
|
||||
{translateUsingReactNode('scenes.apps.messages.message.parts.deleted_content.text', [
|
||||
clickableFullName,
|
||||
])}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
.first_message {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-bottom: 10vh;
|
||||
padding-top: 10vh;
|
||||
|
||||
.emojione {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.icon {
|
||||
display: inline-block;
|
||||
font-size: 88px;
|
||||
}
|
||||
|
||||
.channel_first_message_icon {
|
||||
.ant-avatar-group .ant-avatar:not(:first-child) {
|
||||
margin-left: -32px !important;
|
||||
}
|
||||
|
||||
.user_image {
|
||||
width: 78px;
|
||||
height: 78px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-color: var(--white);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
padding-bottom: 20px;
|
||||
border: 2px solid #fff;
|
||||
margin-left: -40px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-top: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
|
||||
import Emojione from 'components/emojione/emojione';
|
||||
import './FirstMessage.scss';
|
||||
|
||||
export default React.memo(() => {
|
||||
return (
|
||||
<div className="first_message">
|
||||
<div className="content">
|
||||
<div className="icon">
|
||||
<Emojione s128 type={'🥇'} />
|
||||
</div>
|
||||
<div className="text">
|
||||
{Languages.t('scenes.apps.messages.message.types.first_channel_message_text')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import './FirstMessage.scss';
|
||||
|
||||
export default ({ noReplies }: { noReplies?: boolean }) => {
|
||||
return (
|
||||
<div className="first_message">
|
||||
<div className="content">
|
||||
<div className="text">
|
||||
{noReplies
|
||||
? Languages.t('scenes.apps.messages.message.types.no_message_in_thread')
|
||||
: Languages.t('scenes.apps.messages.message.types.first_message_text')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
.link-preview {
|
||||
border-radius: 0px !important;
|
||||
border: 4px solid var(--primary) !important;
|
||||
border-width: 0 0 0 4px !important;
|
||||
margin-top: 15px !important;
|
||||
padding-left: 15px !important;
|
||||
|
||||
.delete-link-preview {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
top: -8px;
|
||||
color: var(--grey) !important;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover .delete-link-preview {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.ant-card .ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.ant-card .ant-card-meta-avatar {
|
||||
padding-right: 0px;
|
||||
width: 100%;
|
||||
margin-bottom: 0px;
|
||||
|
||||
span > img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.ant-avatar {
|
||||
text-align: start;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-preview-domain {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.preview-title > a {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.ant-card-meta-description {
|
||||
margin-bottom: 10px;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.ant-card-cover {
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
border-radius: 8px 8px 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { MessageLinkType } from 'app/features/messages/types/message';
|
||||
import './LinkPreview.scss';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import User from 'app/features/users/services/current-user-service';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
import { X } from 'react-feather';
|
||||
|
||||
type PropsType = {
|
||||
preview: MessageLinkType;
|
||||
};
|
||||
|
||||
export default ({ preview }: PropsType): React.ReactElement => {
|
||||
const context = useContext(MessageContext);
|
||||
const { deleteLinkPreview, message } = useMessage(context);
|
||||
|
||||
return (
|
||||
<div className="xs:max-w-xs max-w-md ant-card ant-card-bordered ant-card-small ant-card-type-inner link-preview">
|
||||
{message.user_id === User.getCurrentUserId() ? (
|
||||
<div className="delete-link-preview">
|
||||
<X size={16} onClick={() => deleteLinkPreview(preview.url)} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="ant-card-body">
|
||||
<div className="ant-card-meta">
|
||||
<div className="ant-card-meta-detail">
|
||||
<div className="ant-card-meta-avatar">
|
||||
{preview.favicon && (
|
||||
<span className="ant-avatar ant-avatar-circle ant-avatar-image">
|
||||
<img alt={preview.domain} src={preview.favicon} />
|
||||
</span>
|
||||
)}
|
||||
<span className="link-preview-domain">{preview.domain}</span>
|
||||
</div>
|
||||
<div className="preview-title">
|
||||
<a
|
||||
href={preview.url?.replace(/^javascript:/, '')}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="truncate text-ellipsis w-full"
|
||||
>
|
||||
{preview.title}
|
||||
</a>
|
||||
</div>
|
||||
<div className="ant-card-meta-description">{preview.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
{preview.img && (
|
||||
<div className="ant-card-cover">
|
||||
<img
|
||||
alt={preview.title}
|
||||
src={preview.img}
|
||||
onClick={() => window.open(preview.url, '_blank')}
|
||||
style={{
|
||||
maxWidth: preview.img_width ?? '100%',
|
||||
maxHeight: preview.img_height ?? '100%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import { useThreadMessages } from 'app/features/messages/hooks/use-thread-messages';
|
||||
import React, { useContext } from 'react';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import ThreadSection from '../../parts/thread-section';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
import { NodeMessage } from 'app/features/messages/types/message';
|
||||
|
||||
export default (props: { firstMessageId: string; onFirstMessageChanged: (item: NodeMessage) => void }) => {
|
||||
const context = useContext(MessageContext);
|
||||
const { message } = useMessage(context);
|
||||
const { messages, window, loadMore } = useThreadMessages({
|
||||
companyId: context.companyId,
|
||||
threadId: message.thread_id,
|
||||
});
|
||||
|
||||
const loadMoreMessages = async (direction: 'history' | 'future') => {
|
||||
const messages = await loadMore(direction, 10, props.firstMessageId);
|
||||
props.onFirstMessageChanged && props.onFirstMessageChanged(messages[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!(window.reachedStart && window.start === props.firstMessageId) &&
|
||||
window.end &&
|
||||
messages.length < message.stats.replies && (
|
||||
<ThreadSection gradient>
|
||||
<div className="message-content">
|
||||
<span onClick={() => loadMoreMessages('history')} className="link">
|
||||
{Languages.t('scenes.apps.messages.message.show_responses_button')} (
|
||||
{Math.max(message.stats.replies, messages.length)})
|
||||
</span>
|
||||
</div>
|
||||
</ThreadSection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { Row } from 'antd';
|
||||
import { useUploadZones } from 'app/features/files/hooks/use-upload-zones';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import 'moment-timezone';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
import PossiblyPendingAttachment from './PossiblyPendingAttachment';
|
||||
|
||||
export default () => {
|
||||
const context = useContext(MessageContext);
|
||||
const { message, save } = useMessage(context);
|
||||
|
||||
const { files, setFiles } = useUploadZones(`message-${message.id}`);
|
||||
|
||||
useEffect(() => {
|
||||
setFiles(message.files || []);
|
||||
}, [message.files]);
|
||||
|
||||
useEffect(() => {
|
||||
const uploaded = files.filter(f => f.metadata?.source !== 'pending');
|
||||
const inMessage = (message.files || []).filter(f => f.metadata?.source !== 'pending');
|
||||
if (uploaded.length > inMessage.length) {
|
||||
save({ ...message, files: uploaded });
|
||||
}
|
||||
}, [files]);
|
||||
|
||||
if (files.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row justify="start" align="middle" className="small-top-margin" wrap>
|
||||
{files
|
||||
.filter(f => f.metadata)
|
||||
.map(file => (
|
||||
<PossiblyPendingAttachment
|
||||
key={file.metadata?.external_id || file.id}
|
||||
type={'message'}
|
||||
file={file}
|
||||
xlarge={files.length === 1 && (files[0].metadata?.thumbnails?.length || 0) > 0}
|
||||
large={
|
||||
//If all the documents are images
|
||||
files.length <= 6 &&
|
||||
files.filter(
|
||||
file =>
|
||||
file.metadata?.source === 'internal' &&
|
||||
(file.metadata?.thumbnails?.length || 0) > 0,
|
||||
).length === files.length
|
||||
}
|
||||
onRemove={() => setFiles(files.filter(f => f.id !== file.id))}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
@@ -1,250 +0,0 @@
|
||||
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
|
||||
import { useIsChannelMember } from 'app/features/channels/hooks/use-channel';
|
||||
import PseudoMarkdownCompiler from 'app/features/global/services/pseudo-markdown-compiler-service';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
|
||||
import { MessageWithReplies } from 'app/features/messages/types/message';
|
||||
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
|
||||
import { useUser } from 'app/features/users/hooks/use-user';
|
||||
import User from 'app/features/users/services/current-user-service';
|
||||
import MessageQuote from 'app/molecules/message-quote';
|
||||
import MessageStatus from 'app/molecules/message-status';
|
||||
import QuotedContent, { useQuotedMessage } from 'app/molecules/quoted-content';
|
||||
import { ViewContext } from 'app/views/client/main-view/MainContent';
|
||||
import classNames from 'classnames';
|
||||
import 'moment-timezone';
|
||||
import { ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import { gotoMessage } from 'src/utils/messages';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
import Blocks from './Blocks';
|
||||
import DeletedContent from './DeletedContent';
|
||||
import LinkPreview from './LinkPreview';
|
||||
import MessageForward from './message-forward';
|
||||
import MessageAttachments from './MessageAttachments';
|
||||
import MessageEdition from './MessageEdition';
|
||||
import MessageHeader from './MessageHeader';
|
||||
import Options from './Options';
|
||||
import Reactions from './Reactions';
|
||||
import RetryButtons from './RetryButtons';
|
||||
|
||||
type Props = {
|
||||
linkToThread?: boolean;
|
||||
threadHeader?: string;
|
||||
};
|
||||
|
||||
let loadingInteractionTimeout = 0;
|
||||
|
||||
export default (props: Props) => {
|
||||
const [active, setActive] = useState(false);
|
||||
const [loadingAction, setLoadingAction] = useState(false);
|
||||
const [didMouseOver, setDidMouseOver] = useState(false);
|
||||
|
||||
const context = useContext(MessageContext);
|
||||
const channelId = context.channelId;
|
||||
const { message } = useMessage(context);
|
||||
|
||||
// Quoted message logic
|
||||
const quotedMessage = useQuotedMessage(message, context);
|
||||
const showQuotedMessage = quotedMessage && quotedMessage.thread_id;
|
||||
let authorName = '';
|
||||
const currentRouterWorkspace = useRouterWorkspace();
|
||||
const workspaceId =
|
||||
context.workspaceId === 'direct' ? currentRouterWorkspace : context.workspaceId;
|
||||
const deletedQuotedMessage = quotedMessage && quotedMessage.subtype === 'deleted';
|
||||
|
||||
if (showQuotedMessage) {
|
||||
const author = useUser(quotedMessage.user_id || '');
|
||||
authorName = author ? User.getFullName(author) : 'Anonymous';
|
||||
}
|
||||
|
||||
const onInteractiveMessageAction = (action_id: string, context: unknown, passives: unknown) => {
|
||||
const app_id = message.application_id;
|
||||
const type = 'interactive_message_action';
|
||||
const event = action_id;
|
||||
const data = {
|
||||
interactive_context: context,
|
||||
form: passives,
|
||||
message: message,
|
||||
};
|
||||
WorkspacesApps.notifyApp(app_id, type, event, data);
|
||||
};
|
||||
|
||||
const onAction = (type: string, id: string, context: unknown, passives: unknown) => {
|
||||
if (type === 'interactive_action') {
|
||||
setLoadingAction(true);
|
||||
clearTimeout(loadingInteractionTimeout);
|
||||
loadingInteractionTimeout = window.setTimeout(() => {
|
||||
setLoadingAction(false);
|
||||
}, 5000);
|
||||
onInteractiveMessageAction(id, context, passives);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingAction(false);
|
||||
}, [JSON.stringify(message.blocks)]);
|
||||
|
||||
const deleted = message.subtype === 'deleted';
|
||||
|
||||
const location = `message-${message.id}`;
|
||||
const { active: editorIsActive } = useVisibleMessagesEditorLocation(
|
||||
location,
|
||||
useContext(ViewContext).type,
|
||||
);
|
||||
|
||||
const showEdition = !props.linkToThread && editorIsActive;
|
||||
const messageIsLoading = message._status === 'sending';
|
||||
const messageSaveFailed = message._status === 'failed';
|
||||
|
||||
const isChannelMember = useIsChannelMember(channelId);
|
||||
const quotedContent = <QuotedContent message={quotedMessage} />;
|
||||
const showMessageStatus = message.user_id === User.getCurrentUserId();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('message-content', {
|
||||
active,
|
||||
'loading-interaction': loadingAction,
|
||||
'link-to-thread': props.linkToThread,
|
||||
})}
|
||||
onMouseEnter={() => {
|
||||
setDidMouseOver(true);
|
||||
}}
|
||||
onClick={() => setActive(false)}
|
||||
key={`message_container_${message.id}`}
|
||||
>
|
||||
<MessageHeader linkToThread={props.linkToThread} />
|
||||
{showQuotedMessage && !showEdition && quotedMessage.channel_id === context.channelId && (
|
||||
<MessageQuote
|
||||
className="mb-1"
|
||||
author={authorName}
|
||||
message={quotedContent}
|
||||
closable={false}
|
||||
deleted={deletedQuotedMessage}
|
||||
goToMessage={() =>
|
||||
gotoMessage(
|
||||
quotedMessage,
|
||||
quotedMessage.company_id || context.companyId,
|
||||
quotedMessage.channel_id || context.channelId,
|
||||
quotedMessage.workspace_id || workspaceId,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showQuotedMessage && !showEdition && quotedMessage.channel_id !== context.channelId && (
|
||||
<MessageForward
|
||||
onAction={(type: string, id: string, context: unknown, passives: unknown) => {
|
||||
if (isChannelMember) onAction(type, id, context, passives);
|
||||
}}
|
||||
className="mb-1"
|
||||
author={authorName}
|
||||
message={quotedMessage}
|
||||
closable={false}
|
||||
deleted={deletedQuotedMessage}
|
||||
goToMessage={() =>
|
||||
gotoMessage(
|
||||
quotedMessage,
|
||||
quotedMessage.company_id || context.companyId,
|
||||
quotedMessage.channel_id || context.channelId,
|
||||
quotedMessage.workspace_id || workspaceId,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!!showEdition && !deleted && (
|
||||
<div className="content-parent pt-1">
|
||||
<MessageEdition />
|
||||
</div>
|
||||
)}
|
||||
{!showEdition && (
|
||||
<MessageBlockContent
|
||||
onAction={(type: string, id: string, context: unknown, passives: unknown) => {
|
||||
if (isChannelMember) onAction(type, id, context, passives);
|
||||
}}
|
||||
deleted={deleted}
|
||||
linkToThread={props.linkToThread}
|
||||
message={message}
|
||||
className={classNames({
|
||||
message_is_loading: messageIsLoading,
|
||||
'message-not-sent': messageSaveFailed,
|
||||
})}
|
||||
suffix={
|
||||
<>
|
||||
{message?.files && (message?.files?.length || 0) > 0 && <MessageAttachments />}
|
||||
{!messageSaveFailed && <Reactions />}
|
||||
{messageSaveFailed && !messageIsLoading && <RetryButtons />}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isChannelMember &&
|
||||
!showEdition &&
|
||||
!deleted &&
|
||||
!messageSaveFailed &&
|
||||
didMouseOver &&
|
||||
!messageIsLoading && (
|
||||
<Options
|
||||
onOpen={() => setActive(true)}
|
||||
onClose={() => setActive(false)}
|
||||
threadHeader={props.threadHeader}
|
||||
key={`options_${message.id}`}
|
||||
/>
|
||||
)}
|
||||
{showMessageStatus && !showEdition && (
|
||||
<MessageStatus key={`message_status_${message.id}`} status={message.status} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MessageBlockContent = ({
|
||||
deleted,
|
||||
message,
|
||||
linkToThread,
|
||||
suffix,
|
||||
className,
|
||||
onAction,
|
||||
}: {
|
||||
deleted: boolean;
|
||||
linkToThread?: boolean;
|
||||
message: MessageWithReplies;
|
||||
suffix?: ReactNode;
|
||||
className?: string;
|
||||
onAction: (type: string, id: string, context: unknown, passives: unknown) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="content-parent dont-break-out">
|
||||
{deleted === true ? (
|
||||
<div className="deleted-message">
|
||||
<DeletedContent userId={message.user_id || ''} key={`deleted_${message.thread_id}`} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={'content allow_selection' + (className || '')}>
|
||||
{!!linkToThread && message.text}
|
||||
{!linkToThread && (
|
||||
<>
|
||||
<Blocks
|
||||
blocks={message.blocks}
|
||||
fallback={PseudoMarkdownCompiler.transformBackChannelsUsers(message.text)}
|
||||
onAction={onAction}
|
||||
allowAdvancedBlocks={message.subtype === 'application'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{message?.links &&
|
||||
(message?.links?.length || 0) > 0 &&
|
||||
message.links
|
||||
.filter(link => link && (link.title || link.description || link.img))
|
||||
.map((preview, i) => (
|
||||
<LinkPreview key={`${i}-${preview.url}-${message.thread_id}`} preview={preview} />
|
||||
))}
|
||||
|
||||
{suffix}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
.message-edition {
|
||||
padding-top: 4px;
|
||||
|
||||
.message-edition-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.editor-toolbar {
|
||||
padding-left: 0;
|
||||
|
||||
.button-group:first-child {
|
||||
margin-left: -8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import 'moment-timezone';
|
||||
import MessageInput from '../../input/input';
|
||||
import Button from 'components/buttons/button.jsx';
|
||||
import AlertManager from 'app/features/global/services/alert-manager-service';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import EditorToolbar from 'app/components/rich-text-editor/editor-toolbar';
|
||||
import RichTextEditorStateService from 'app/components/rich-text-editor/editor-state-service';
|
||||
import './MessageEdition.scss';
|
||||
import { EditorState } from 'draft-js';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
import { ViewContext } from 'app/views/client/main-view/MainContent';
|
||||
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* The editor plugins to enable during edition
|
||||
*/
|
||||
editorPlugins?: string[];
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const context = useContext(MessageContext);
|
||||
const { message, remove, save: updateMessage } = useMessage(context);
|
||||
|
||||
const location = `message-${context.id}`;
|
||||
const subLocation = useContext(ViewContext).type;
|
||||
const editorId = `thread:${context.threadId}/message:${context.id}`;
|
||||
const { close } = useVisibleMessagesEditorLocation(location, subLocation);
|
||||
|
||||
const editorPlugins = props.editorPlugins || ['emoji', 'mention', 'channel'];
|
||||
const format = 'markdown';
|
||||
|
||||
const [editorState, setEditorState] = useState<EditorState>();
|
||||
const [isReady, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const dataParser = RichTextEditorStateService.getDataParser(editorPlugins);
|
||||
const initialContent = message?.text || '';
|
||||
|
||||
setEditorState(() =>
|
||||
RichTextEditorStateService.get(editorId, {
|
||||
plugins: editorPlugins,
|
||||
clearIfExists: true,
|
||||
initialContent: dataParser.fromString(initialContent, format),
|
||||
}),
|
||||
);
|
||||
|
||||
setReady(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const getContentOutput = (editorState: EditorState) => {
|
||||
return RichTextEditorStateService.getDataParser(editorPlugins).toString(editorState, format);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
let content = null;
|
||||
if (editorState) content = getContentOutput(editorState);
|
||||
if (!content) {
|
||||
AlertManager.confirm(
|
||||
() => {
|
||||
remove();
|
||||
},
|
||||
() => undefined,
|
||||
{
|
||||
title: Languages.t('scenes.apps.messages.chatbox.chat.delete_message_btn', [], 'Delete'),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
updateMessage({ ...message, text: content });
|
||||
}
|
||||
close();
|
||||
};
|
||||
|
||||
const setRichTextEditorState = (editorState: EditorState): void => {
|
||||
setEditorState(editorState);
|
||||
RichTextEditorStateService.set(editorId, editorState);
|
||||
};
|
||||
|
||||
return !isReady || !editorState ? (
|
||||
<></>
|
||||
) : (
|
||||
<div className="message-edition">
|
||||
<MessageInput
|
||||
threadId={context.threadId}
|
||||
messageId={context.id}
|
||||
context={'edition'}
|
||||
onSend={() => save()}
|
||||
editorState={editorState}
|
||||
onChange={editorState => {
|
||||
setRichTextEditorState(editorState);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="message-edition-toolbar message-input px-1 pl-2 mt-1">
|
||||
<div className="input-options-toolbar">
|
||||
<div className="richtext-toolbar input-toolbar fade_in">
|
||||
<EditorToolbar
|
||||
editorState={editorState}
|
||||
onChange={editorState => {
|
||||
setRichTextEditorState(editorState);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="message-edition-buttons">
|
||||
<Button
|
||||
className="primary small-right-margin"
|
||||
small
|
||||
onClick={async () => {
|
||||
save();
|
||||
}}
|
||||
value={Languages.t('scenes.apps.messages.message.save_button', [], 'Save')}
|
||||
></Button>
|
||||
|
||||
<Button
|
||||
className="secondary-light"
|
||||
small
|
||||
onClick={() => close()}
|
||||
value={Languages.t('scenes.apps.messages.message.cancel_button', [], 'Cancel')}
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,159 +0,0 @@
|
||||
import React, { ReactNode, useContext, useState } from 'react';
|
||||
|
||||
import 'moment-timezone';
|
||||
import Moment from 'react-moment';
|
||||
import { Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { AlertTriangle } from 'react-feather';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import User from 'app/features/users/services/current-user-service';
|
||||
import MenusManager from 'app/components/menus/menus-manager.jsx';
|
||||
import UserCard from 'app/components/user-card/user-card';
|
||||
import Emojione from 'components/emojione/emojione';
|
||||
import RouterServices from 'app/features/router/services/router-service';
|
||||
import { NodeMessage } from 'app/features/messages/types/message';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import Loader from 'components/loader/loader.jsx';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
|
||||
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
|
||||
import { useUser } from 'app/features/users/hooks/use-user';
|
||||
import { UserType } from 'app/features/users/types/user';
|
||||
import { CompanyApplicationsStateFamily } from 'app/features/applications/state/company-applications';
|
||||
import { useDirectChannels } from 'app/features/channels/hooks/use-direct-channels';
|
||||
import { addUrlTryDesktop } from 'app/views/desktop-redirect';
|
||||
|
||||
type Props = {
|
||||
linkToThread?: boolean;
|
||||
};
|
||||
|
||||
const { Link } = Typography;
|
||||
|
||||
export default (props: Props) => {
|
||||
const channelId = useRouterChannel();
|
||||
const workspaceId = useRouterWorkspace();
|
||||
const [messageLink, setMessageLink] = useState('');
|
||||
const { openDiscussion } = useDirectChannels();
|
||||
|
||||
const context = useContext(MessageContext);
|
||||
const { message } = useMessage(context);
|
||||
const parentMessage: NodeMessage | null = useMessage({
|
||||
...context,
|
||||
id: message.thread_id,
|
||||
}).message;
|
||||
|
||||
const user = useUser(message.user_id);
|
||||
|
||||
const companyApplications =
|
||||
useRecoilState(CompanyApplicationsStateFamily(context.companyId))[0] || [];
|
||||
const application = companyApplications.find(a => a.id === message.application_id);
|
||||
|
||||
const scrollToMessage = () => {
|
||||
if (message.thread_id !== message.id) {
|
||||
//TODO messageService.scrollTo({ id: message.thread_id });
|
||||
}
|
||||
};
|
||||
|
||||
const updateMessageLink = () => {
|
||||
const url = RouterServices.generateRouteFromState({
|
||||
workspaceId: workspaceId,
|
||||
channelId: channelId,
|
||||
messageId: message.thread_id || message.id,
|
||||
});
|
||||
setMessageLink(url);
|
||||
};
|
||||
|
||||
let userNameRef: ReactNode = null;
|
||||
const displayUserCard = () => {
|
||||
if (user) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const box = (window as any).getBoundingClientRect(userNameRef);
|
||||
MenusManager.openMenu(
|
||||
[
|
||||
{
|
||||
type: 'react-element',
|
||||
reactElement: () => (
|
||||
<UserCard
|
||||
user={user as UserType}
|
||||
onClick={() => openDiscussion([(user as UserType).id || ''])}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
box,
|
||||
null,
|
||||
{ margin: 8 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const icon = user?.status ? user.status.split(' ')[0] : undefined;
|
||||
const status = user?.status ? user.status.split(' ').splice(1).join(' ') : undefined;
|
||||
return (
|
||||
<div
|
||||
className={classNames('message-content-header-container', {
|
||||
'message-not-sent': message._status === 'failed',
|
||||
})}
|
||||
>
|
||||
<div className={'message-content-header '}>
|
||||
<span
|
||||
className="sender-name"
|
||||
ref={node => (userNameRef = node as ReactNode)}
|
||||
onClick={() => displayUserCard()}
|
||||
>
|
||||
{message.override?.title ||
|
||||
(!!user && User.getFullName(user)) ||
|
||||
(message.application_id && application?.identity?.name)}
|
||||
</span>
|
||||
{!!user && (
|
||||
<div className="sender-status">
|
||||
{!!icon && <Emojione size={12} type={icon} />} {!!status && status}
|
||||
</div>
|
||||
)}
|
||||
{props.linkToThread && (
|
||||
<span className="reply-text">
|
||||
{Languages.t('scenes.apps.messages.input.replied_to')}
|
||||
<Link onClick={() => scrollToMessage()}>{parentMessage?.text}</Link>
|
||||
</span>
|
||||
)}
|
||||
{message.created_at && (
|
||||
<a
|
||||
className="date"
|
||||
// eslint-disable-next-line react/jsx-no-target-blank
|
||||
target="_BLANK"
|
||||
href={(messageLink ? addUrlTryDesktop(messageLink) : '#')?.replace(/^javascript:/, '')}
|
||||
onMouseEnter={() => updateMessageLink()}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Moment
|
||||
format={
|
||||
new Date().getTime() - message.created_at > 12 * 60 * 60 * 1000 ? 'lll' : 'LT'
|
||||
}
|
||||
>
|
||||
{message.created_at}
|
||||
</Moment>
|
||||
|
||||
{message.edited?.edited_at && (
|
||||
<span style={{ textTransform: 'lowercase' }}>
|
||||
{' '}
|
||||
- {Languages.t('scenes.apps.messages.input.edited', [], 'Edited')}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{message._status === 'sending' && (
|
||||
<div className="loading">
|
||||
<Loader color="#999" className="message_header_loader" />
|
||||
</div>
|
||||
)}
|
||||
{message._status === 'failed' && (
|
||||
<div className="alert_failed_icon">
|
||||
<AlertTriangle size={16} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,379 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import 'moment-timezone';
|
||||
import { MoreHorizontal, Smile, ArrowUpRight, Trash2, CornerDownLeft } from 'react-feather';
|
||||
|
||||
import EmojiPicker from 'components/emoji-picker/emoji-picker.jsx';
|
||||
import Menu from 'components/menus/menu.jsx';
|
||||
import MenusManager from 'app/components/menus/menus-manager.jsx';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import AlertManager from 'app/features/global/services/alert-manager-service';
|
||||
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
|
||||
import WorkspaceUserRights from 'app/features/workspaces/services/workspace-user-rights-service';
|
||||
import User from 'app/features/users/services/current-user-service';
|
||||
import RouterServices from 'app/features/router/services/router-service';
|
||||
import { Application } from 'app/features/applications/types/application';
|
||||
import { getCompanyApplications } from 'app/features/applications/state/company-applications';
|
||||
import Groups from 'app/deprecated/workspaces/groups.js';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
|
||||
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
|
||||
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
|
||||
import { ViewContext } from 'app/views/client/main-view/MainContent';
|
||||
import SideViewService from 'app/features/router/services/side-view-service';
|
||||
import Emojione from 'app/components/emojione/emojione';
|
||||
import { useChannel } from 'app/features/channels/hooks/use-channel';
|
||||
import { useEphemeralMessages } from 'app/features/messages/hooks/use-ephemeral-messages';
|
||||
import { copyToClipboard } from 'app/features/global/utils/CopyClipboard';
|
||||
import { addUrlTryDesktop } from 'app/views/desktop-redirect';
|
||||
import { useMessageQuoteReply } from 'app/features/messages/hooks/use-message-quote-reply';
|
||||
import { useMessageSeenBy } from 'app/features/messages/hooks/use-message-seen-by';
|
||||
import { EmojiSuggestionType } from 'app/components/rich-text-editor/plugins/emoji';
|
||||
import { MessagesListContext } from '../../messages-list';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { ForwardMessageAtom } from 'app/components/forward-message';
|
||||
|
||||
type Props = {
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
threadHeader?: string;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const channelId = useRouterChannel();
|
||||
const workspaceId = useRouterWorkspace();
|
||||
const context = useContext(MessageContext);
|
||||
const listContext = useContext(MessagesListContext);
|
||||
const { message, react, remove, pin } = useMessage(context);
|
||||
const { channel } = useChannel(channelId);
|
||||
const { message: thread } = useMessage({
|
||||
companyId: channel.company_id || '',
|
||||
threadId: message.thread_id,
|
||||
id: message.thread_id,
|
||||
});
|
||||
const { remove: removeLastEphemeral } = useEphemeralMessages({
|
||||
companyId: context.companyId,
|
||||
channelId: channelId,
|
||||
});
|
||||
const location = `message-${message.id}`;
|
||||
const subLocation = useContext(ViewContext).type;
|
||||
const { set: setVisibleEditor } = useVisibleMessagesEditorLocation(location, subLocation);
|
||||
|
||||
const { set: setQuoteReply } = useMessageQuoteReply(channelId);
|
||||
const setForwardMessage = useSetRecoilState(ForwardMessageAtom);
|
||||
|
||||
const { openSeenBy } = useMessageSeenBy();
|
||||
|
||||
const menu: Record<string, string | (() => void)>[] = [];
|
||||
|
||||
const triggerApp = (app: Application) => {
|
||||
const data = {
|
||||
channel: channel,
|
||||
thread: thread.id && thread.id !== message.id ? thread : null,
|
||||
message: message,
|
||||
};
|
||||
WorkspacesApps.notifyApp(app.id, 'action', 'action', data);
|
||||
};
|
||||
|
||||
const onOpen = (evt: Event) => {
|
||||
props.onOpen && props.onOpen();
|
||||
if (evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
if (message.ephemeral) {
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
icon: 'trash',
|
||||
text: Languages.t('scenes.apps.messages.message.remove_button', [], 'Delete'),
|
||||
className: 'error',
|
||||
onClick: () => {
|
||||
removeLastEphemeral();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (channel && channel.visibility !== 'direct') {
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
icon: 'arrow-up-right',
|
||||
text: Languages.t('scenes.apps.messages.message.show_button', [], 'Display'),
|
||||
onClick: () => {
|
||||
SideViewService.select(channel?.id || '', {
|
||||
app: { identity: { code: 'messages' } } as Application,
|
||||
context: {
|
||||
viewType: 'channel_thread',
|
||||
threadId: message.thread_id,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
icon: 'link',
|
||||
text: Languages.t('scenes.apps.messages.message.copy_link', [], 'Copy link to message'),
|
||||
onClick: () => {
|
||||
const url = addUrlTryDesktop(
|
||||
`${document.location.origin}${RouterServices.generateRouteFromState({
|
||||
workspaceId: workspaceId,
|
||||
channelId: channelId,
|
||||
threadId: message.thread_id,
|
||||
messageId: message.id,
|
||||
})}`,
|
||||
);
|
||||
|
||||
copyToClipboard(url);
|
||||
},
|
||||
});
|
||||
|
||||
if (channel && channel.visibility === 'direct' && !listContext.readonly) {
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
icon: 'corner-down-left',
|
||||
text: Languages.t('scenes.apps.messages.message.reply_button', [], 'Reply'),
|
||||
onClick: () => {
|
||||
setQuoteReply({ message: message.thread_id, channel: channelId });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
icon: 'comment-info',
|
||||
text: Languages.t('components.message_seen_by.btn', [], 'Information'),
|
||||
onClick: () => {
|
||||
openSeenBy({
|
||||
message_id: message.id,
|
||||
company_id: context.companyId,
|
||||
thread_id: message.thread_id,
|
||||
workspace_id: context.workspaceId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!message.context?.disable_pin && !listContext.readonly)
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
icon: 'map-pin',
|
||||
text: Languages.t(
|
||||
!message.pinned_info?.pinned_at
|
||||
? 'scenes.apps.messages.message.pin_button'
|
||||
: 'scenes.apps.messages.message.unpin_button',
|
||||
[],
|
||||
'Pin message',
|
||||
),
|
||||
className: 'option_button',
|
||||
onClick: () => {
|
||||
pin(!message.pinned_info?.pinned_at);
|
||||
},
|
||||
});
|
||||
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
icon: 'envelope-send',
|
||||
text: Languages.t('scenes.apps.messages.message.forward'),
|
||||
className: 'option_button',
|
||||
onClick: () => {
|
||||
setForwardMessage({
|
||||
id: message.id,
|
||||
thread_id: message.thread_id,
|
||||
channel_id: channelId,
|
||||
workspace_id: context.workspaceId,
|
||||
company_id: context.companyId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const apps =
|
||||
getCompanyApplications(Groups.currentGroupId).filter(
|
||||
(app: Application) => app.display?.twake?.chat?.actions?.length,
|
||||
) || [];
|
||||
|
||||
if (apps.length > 0 && !listContext.readonly) {
|
||||
menu.push({ type: 'separator' });
|
||||
menu.push({
|
||||
type: 'react-element',
|
||||
reactElement: () => {
|
||||
return apps.map((app: Application) => {
|
||||
return (
|
||||
<div
|
||||
key={app.id}
|
||||
className="menu"
|
||||
onClick={() => {
|
||||
triggerApp(app);
|
||||
}}
|
||||
>
|
||||
<div className="text">
|
||||
<div
|
||||
className="menu-app-icon"
|
||||
style={{ backgroundImage: 'url(' + app.identity?.icon + ')' }}
|
||||
/>
|
||||
{app?.display?.twake?.chat?.actions?.[0].description || app.identity?.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(message.user_id === User.getCurrentUserId() ||
|
||||
(message.application_id && message.context?.allow_delete === 'everyone') ||
|
||||
(message.application_id &&
|
||||
WorkspaceUserRights.hasWorkspacePrivilege() &&
|
||||
message.context?.allow_delete === 'administrators')) &&
|
||||
!listContext.readonly
|
||||
) {
|
||||
if (menu.length > 0 && (!message.application_id || !message?.stats?.replies)) {
|
||||
menu.push({ type: 'separator' });
|
||||
}
|
||||
if (!message.application_id) {
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
icon: 'edit-alt',
|
||||
text: Languages.t('scenes.apps.messages.message.modify_button', [], 'Edit'),
|
||||
onClick: () => {
|
||||
setVisibleEditor({ location, subLocation });
|
||||
},
|
||||
});
|
||||
}
|
||||
if (message?.stats?.replies <= 1) {
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
icon: 'trash-alt',
|
||||
text: Languages.t('scenes.apps.messages.message.remove_button', [], 'Delete'),
|
||||
className: 'error',
|
||||
onClick: () => {
|
||||
AlertManager.confirm(() => remove());
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.ephemeral) {
|
||||
return (
|
||||
<div className="message-options right">
|
||||
<div
|
||||
className="option"
|
||||
onClick={() => {
|
||||
removeLastEphemeral();
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const userReactions = (message.reactions || [])?.filter(r =>
|
||||
r.users.includes(User.getCurrentUserId()),
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/*!props.threadHeader && (
|
||||
<div className="message-options drag" key="drag">
|
||||
<div className="option js-drag-handler-message">
|
||||
<DragIndicator style={{ width: '18px' }} />
|
||||
</div>
|
||||
</div>
|
||||
)*/}
|
||||
<div className="message-options right" key="options">
|
||||
{!listContext.readonly && (
|
||||
<>
|
||||
{[':heart:', ':+1:', ':eyes:', ':tada:'].map(emoji => (
|
||||
<>
|
||||
<div
|
||||
key={emoji}
|
||||
className={
|
||||
'option ' + (userReactions.map(m => m.name).includes(emoji) ? 'active' : '')
|
||||
}
|
||||
onClick={() => react([emoji], 'toggle')}
|
||||
>
|
||||
<Emojione type={emoji} />
|
||||
</div>
|
||||
<div className="separator"></div>
|
||||
</>
|
||||
))}
|
||||
|
||||
<Menu
|
||||
className="option"
|
||||
onOpen={(evt: Event) => onOpen(evt)}
|
||||
menu={[
|
||||
{
|
||||
type: 'react-element',
|
||||
className: 'menu-cancel-margin',
|
||||
reactElement: () => {
|
||||
return (
|
||||
<EmojiPicker
|
||||
selected={userReactions.map(e => e.name) || []}
|
||||
onChange={(emoji: EmojiSuggestionType) => {
|
||||
MenusManager.closeMenu();
|
||||
props.onClose && props.onClose();
|
||||
react([emoji.colons], 'toggle');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
position="top"
|
||||
>
|
||||
<Smile size={16} />
|
||||
</Menu>
|
||||
<div className="separator"></div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!props.threadHeader && channel && channel.visibility !== 'direct' && (
|
||||
<>
|
||||
<div
|
||||
className="option"
|
||||
onClick={() => {
|
||||
SideViewService.select(channelId, {
|
||||
app: { identity: { code: 'messages' } } as Application,
|
||||
context: {
|
||||
viewType: 'channel_thread',
|
||||
threadId: message.thread_id || message.id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ArrowUpRight size={16} />
|
||||
</div>
|
||||
<div className="separator"></div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{channel && channel.visibility === 'direct' && !listContext.readonly && (
|
||||
<>
|
||||
<div
|
||||
className="option"
|
||||
onClick={() => {
|
||||
setQuoteReply({ message: message.thread_id, channel: channelId });
|
||||
}}
|
||||
>
|
||||
<CornerDownLeft size={16} />
|
||||
</div>
|
||||
<div className="separator"></div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Menu
|
||||
className="option"
|
||||
onOpen={(evt: Event) => onOpen(evt)}
|
||||
onClose={() => props.onClose && props.onClose()}
|
||||
menu={menu}
|
||||
position={'left'}
|
||||
>
|
||||
<MoreHorizontal size={16} />
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useUpload } from 'app/features/files/hooks/use-upload';
|
||||
import FileComponent from 'app/components/file/file-component';
|
||||
import { DataFileType } from 'app/components/file/types';
|
||||
import { PendingFileRecoilType } from 'app/features/files/types/file';
|
||||
import FileUploadAPIClient from 'app/features/files/api/file-upload-api-client';
|
||||
import { MessageFileType } from 'app/features/messages/types/message';
|
||||
|
||||
type PropsType = {
|
||||
file: MessageFileType;
|
||||
onRemove?: () => void;
|
||||
type: 'input' | 'message';
|
||||
large?: boolean;
|
||||
xlarge?: boolean;
|
||||
};
|
||||
|
||||
export default ({ file, onRemove, type, large, xlarge }: PropsType) => {
|
||||
const { getOnePendingFile } = useUpload();
|
||||
|
||||
const id =
|
||||
(typeof file.metadata?.external_id === 'string'
|
||||
? file.metadata?.external_id
|
||||
: file.metadata?.external_id?.id) || '';
|
||||
const companyId =
|
||||
(typeof file.metadata?.external_id === 'string'
|
||||
? file.company_id
|
||||
: file.metadata?.external_id?.company_id) || '';
|
||||
|
||||
let status: PendingFileRecoilType['status'] | undefined = 'success';
|
||||
let progress = 1;
|
||||
|
||||
let formatedFile: DataFileType = {
|
||||
id: id,
|
||||
company_id: companyId,
|
||||
name: file.metadata?.name || '',
|
||||
size: file.metadata?.size || 0,
|
||||
thumbnail: FileUploadAPIClient.getFileThumbnailUrlFromMessageFile(file) || '',
|
||||
thumbnail_ratio:
|
||||
(file.metadata?.thumbnails?.[0]?.width || 1) / (file.metadata?.thumbnails?.[0]?.height || 1),
|
||||
type: FileUploadAPIClient.mimeToType(file.metadata?.mime || ''),
|
||||
};
|
||||
|
||||
if (file?.metadata?.source === 'pending') {
|
||||
const pendingFile = getOnePendingFile(id);
|
||||
if (!pendingFile) {
|
||||
if (onRemove) onRemove();
|
||||
return <></>;
|
||||
}
|
||||
|
||||
formatedFile = {
|
||||
id: pendingFile?.backendFile?.id || '',
|
||||
company_id: pendingFile?.backendFile?.company_id || '',
|
||||
name: pendingFile?.originalFile.name || '',
|
||||
size: pendingFile?.originalFile.size || 0,
|
||||
thumbnail: URL.createObjectURL(pendingFile.originalFile),
|
||||
thumbnail_ratio: 1,
|
||||
type: FileUploadAPIClient.mimeToType(pendingFile?.originalFile.type || ''),
|
||||
};
|
||||
status = pendingFile.status || undefined;
|
||||
progress = pendingFile.progress;
|
||||
}
|
||||
|
||||
return formatedFile ? (
|
||||
<FileComponent
|
||||
className="small-right-margin small-bottom-margin"
|
||||
context={type}
|
||||
source={file.metadata?.source || 'internal'}
|
||||
externalId={file.metadata?.external_id}
|
||||
file={formatedFile}
|
||||
messageFile={file}
|
||||
large={large}
|
||||
xlarge={xlarge}
|
||||
status={status}
|
||||
progress={progress}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import User from 'app/features/users/services/current-user-service';
|
||||
import 'moment-timezone';
|
||||
import Emojione from 'components/emojione/emojione';
|
||||
import { ReactionType } from 'app/features/messages/types/message';
|
||||
import { Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import { getUser } from 'app/features/users/hooks/use-user-list';
|
||||
import { MessagesListContext } from '../../messages-list';
|
||||
|
||||
export default () => {
|
||||
const context = useContext(MessageContext);
|
||||
const { message, react } = useMessage(context);
|
||||
|
||||
if (
|
||||
!(!message?.context?.disable_reactions && message?.reactions && message?.reactions.length > 0)
|
||||
)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<div className="reactions">
|
||||
{(message?.reactions || [])
|
||||
.map(r => r) //To avoid modifing the original one with sort
|
||||
.sort((a, b) => b.count || 0 - a.count)
|
||||
.map((reaction, index) => (
|
||||
<Reaction reaction={reaction} react={react} key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Reaction = ({
|
||||
reaction,
|
||||
react,
|
||||
}: {
|
||||
reaction: ReactionType;
|
||||
react: (emojis: string[], mode?: 'add' | 'toggle' | 'remove' | 'replace') => Promise<void>;
|
||||
}): JSX.Element => {
|
||||
const listContext = useContext(MessagesListContext);
|
||||
const noReactions: boolean = (reaction.count || 0) <= 0;
|
||||
const users: ReactionType['users'] = reaction.users || [];
|
||||
|
||||
if (noReactions) return <></>;
|
||||
|
||||
const reactionClassName = classNames('reaction', {
|
||||
is_selected: reaction.users.includes(User.getCurrentUserId()),
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
className="reaction_container"
|
||||
placement="top"
|
||||
title={<ReactionTooltip users={users} />}
|
||||
>
|
||||
<div
|
||||
className={reactionClassName}
|
||||
onClick={() => {
|
||||
if (!listContext.readonly) react([reaction.name], 'toggle');
|
||||
}}
|
||||
>
|
||||
<Emojione type={reaction.name} />
|
||||
{reaction.count}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const ReactionTooltip = ({ users }: { users: ReactionType['users'] }): JSX.Element => (
|
||||
<>
|
||||
{users.map(id => {
|
||||
const user = getUser(id);
|
||||
|
||||
if (!user) return <></>;
|
||||
|
||||
const name = User.getFullName(user);
|
||||
|
||||
return (
|
||||
<div key={id} style={{ whiteSpace: 'nowrap' }}>
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
@@ -1,69 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { CornerDownRight } from 'react-feather';
|
||||
import ThreadSection from '../../parts/thread-section';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
import { useVisibleMessagesEditorLocation } from 'app/features/messages/hooks/use-message-editor';
|
||||
import { ViewContext } from 'app/views/client/main-view/MainContent';
|
||||
import Input from '../../input/input';
|
||||
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
|
||||
import { getChannel, useIsChannelMember } from 'app/features/channels/hooks/use-channel';
|
||||
|
||||
|
||||
export default () => {
|
||||
const context = useContext(MessageContext);
|
||||
const channelId = useRouterChannel();
|
||||
const { message } = useMessage(context);
|
||||
const channel = getChannel(channelId);
|
||||
|
||||
const location = `thread-${message.thread_id}`;
|
||||
const subLocation = useContext(ViewContext).type;
|
||||
const { active: editorIsActive, set: setVisibleEditor } = useVisibleMessagesEditorLocation(
|
||||
location,
|
||||
subLocation,
|
||||
);
|
||||
|
||||
const isChannelMember = useIsChannelMember(channelId);
|
||||
|
||||
if (!isChannelMember) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (message.subtype === 'deleted' || message.thread_id != message.id) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (channel?.visibility === 'direct') {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (editorIsActive) {
|
||||
return (
|
||||
<ThreadSection small alinea>
|
||||
<div className="message-content">
|
||||
<Input threadId={message?.id || ''} channelId={channelId} />
|
||||
</div>
|
||||
</ThreadSection>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThreadSection compact>
|
||||
<div className="message-content">
|
||||
<span
|
||||
className="link"
|
||||
onClick={() =>
|
||||
setVisibleEditor({
|
||||
location,
|
||||
subLocation,
|
||||
})
|
||||
}
|
||||
>
|
||||
<CornerDownRight size={14} className="inline" />{' '}
|
||||
{Languages.t('scenes.apps.messages.message.reply_button')}
|
||||
</span>
|
||||
</div>
|
||||
</ThreadSection>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import { useMessageEditor } from 'app/features/messages/hooks/use-message-editor';
|
||||
import React, { useContext } from 'react';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { MessageContext } from '../message-with-replies';
|
||||
|
||||
export default () => {
|
||||
const context = useContext(MessageContext);
|
||||
const { message } = useMessage(context);
|
||||
const { retry, cancel } = useMessageEditor(context);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span
|
||||
className="link"
|
||||
style={{ fontWeight: 500, marginRight: 8 }}
|
||||
onClick={() => retry(message)}
|
||||
>
|
||||
{Languages.t('general.retry')}
|
||||
</span>
|
||||
<span className="link red" style={{ fontWeight: 500 }} onClick={() => cancel(message)}>
|
||||
{Languages.t('general.remove')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { Row } from 'antd';
|
||||
import { MessageFileType } from 'app/features/messages/types/message';
|
||||
import PossiblyPendingAttachment from '../PossiblyPendingAttachment';
|
||||
|
||||
export const ForwardedFiles = (props: { files: MessageFileType[] }) => {
|
||||
return (
|
||||
<Row justify="start" align="middle" className="small-top-margin" wrap>
|
||||
{props.files
|
||||
.filter(f => f.metadata)
|
||||
.map(file => (
|
||||
<PossiblyPendingAttachment
|
||||
key={file.metadata?.external_id || file.id}
|
||||
type={'message'}
|
||||
file={file}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import { Info } from 'app/atoms/text';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { XIcon } from '@atoms/icons-agnostic';
|
||||
import { MessageWithReplies, NodeMessage } from 'app/features/messages/types/message';
|
||||
import { UserType } from 'app/features/users/types/user';
|
||||
import { MessageBlockContent } from '../MessageContent';
|
||||
import { ForwardedFiles } from './files';
|
||||
|
||||
type PropsType = {
|
||||
message: NodeMessage & {
|
||||
users?: UserType[] | undefined;
|
||||
company_id: string;
|
||||
workspace_id: string;
|
||||
channel_id: string;
|
||||
thread_id: string;
|
||||
id: string;
|
||||
};
|
||||
author: string;
|
||||
closable?: boolean;
|
||||
deleted?: boolean;
|
||||
goToMessage?: () => void;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
onAction: (type: string, id: string, context: unknown, passives: unknown) => void;
|
||||
};
|
||||
|
||||
export default ({
|
||||
author,
|
||||
message,
|
||||
closable = true,
|
||||
onClose,
|
||||
deleted = false,
|
||||
goToMessage,
|
||||
className = '',
|
||||
onAction,
|
||||
}: PropsType) => {
|
||||
const clickable = !closable;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'flex flex-row pl-3 pr-2 relative ' +
|
||||
(className || '') +
|
||||
' ' +
|
||||
(clickable ? 'cursor-pointer hover:bg-blue-100 hover:bg-opacity-50' : '')
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onClick={!closable ? goToMessage : () => {}}
|
||||
>
|
||||
<div className="w-[3px] rounded-full bg-blue-500 absolute left-0 top-0 h-full"></div>
|
||||
<div className="grow w-full max-w-full">
|
||||
<h3 className="mt-0.5 -mb-0.5 text-xs text-blue-500">{author}</h3>
|
||||
<div className="">
|
||||
{deleted ? (
|
||||
<Info className="italic text-xs">{Languages.t('molecules.message_quote.deleted')}</Info>
|
||||
) : (
|
||||
<MessageBlockContent
|
||||
deleted={false}
|
||||
message={message as unknown as MessageWithReplies}
|
||||
onAction={onAction}
|
||||
suffix={<>{message.files && <ForwardedFiles files={message.files} />}</>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{closable && onClose && (
|
||||
<div className="flex-none w-2">
|
||||
<XIcon
|
||||
className="cursor-pointer mt-1 float-right w-3 h-3 text-blue-500 hover:text-blue-600"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useThreadMessages } from 'app/features/messages/hooks/use-thread-messages';
|
||||
import Message from './message';
|
||||
import { MessageContext } from './message-with-replies';
|
||||
import ThreadSection from '../parts/thread-section';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import Numbers from 'app/features/global/utils/Numbers';
|
||||
|
||||
type Props = {
|
||||
companyId: string;
|
||||
workspaceId: string;
|
||||
channelId: string;
|
||||
threadId: string;
|
||||
firstMessageId: string;
|
||||
};
|
||||
|
||||
export default ({ threadId, companyId, channelId, workspaceId, firstMessageId }: Props) => {
|
||||
const { messages } = useThreadMessages({ companyId, threadId });
|
||||
|
||||
return (
|
||||
<>
|
||||
{messages
|
||||
.filter(m => !firstMessageId || Numbers.compareTimeuuid(m.id, firstMessageId) >= 0)
|
||||
.filter(m => m.threadId !== m.id)
|
||||
.map(m => {
|
||||
return (
|
||||
<MessageContext.Provider
|
||||
key={m.id}
|
||||
value={{ ...m, id: m.id || '', channelId, workspaceId, companyId }}
|
||||
>
|
||||
<Reply />
|
||||
</MessageContext.Provider>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Reply = () => {
|
||||
const context = React.useContext(MessageContext);
|
||||
const { message } = useMessage(context);
|
||||
return (
|
||||
<ThreadSection withAvatar alinea small pinned={!!message.pinned_info?.pinned_by}>
|
||||
<Message />
|
||||
</ThreadSection>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import Moment from 'react-moment';
|
||||
import 'moment-timezone';
|
||||
import './message.scss';
|
||||
|
||||
type Props = {
|
||||
date: number;
|
||||
};
|
||||
|
||||
export default React.memo((props: Props) => {
|
||||
return (
|
||||
<div className="time_separator">
|
||||
<div className="message_timeline">
|
||||
<div className="time_container">
|
||||
<div className="time">
|
||||
{new Date().getTime() - props.date > 24 * 60 * 60 * 1000 ? (
|
||||
<Moment date={props.date || 0} format="LL"></Moment>
|
||||
) : (
|
||||
<Moment date={props.date || 0} fromNow></Moment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,272 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useChannelMessages } from 'app/features/messages/hooks/use-channel-messages';
|
||||
import ListBuilder, { ListBuilderHandle } from './list-builder';
|
||||
import TimeSeparator from './message/time-separator';
|
||||
import MessageWithReplies from './message/message-with-replies';
|
||||
import FirstMessage from './message/parts/FirstMessage/FirstMessage';
|
||||
import LockedHistoryBanner from 'app/components/locked-features-components/locked-history-banner/locked-history-banner';
|
||||
import MessageHistoryService from 'app/features/messages/services/message-history-service';
|
||||
import { useCurrentCompany } from 'app/features/companies/hooks/use-companies';
|
||||
import ChannelAPIClient from 'app/features/channels/api/channel-api-client';
|
||||
import {
|
||||
MessagesAndComponentsType,
|
||||
withNonMessagesComponents,
|
||||
} from '../../../features/messages/hooks/with-non-messages-components';
|
||||
import { useHighlightMessage } from 'app/features/messages/hooks/use-highlight-message';
|
||||
import SideViewService from 'app/features/router/services/side-view-service';
|
||||
import { Application } from 'app/features/applications/types/application';
|
||||
import GoToBottom from './parts/go-to-bottom';
|
||||
import { MessagesPlaceholder } from './placeholder';
|
||||
import { cleanFrontMessagesFromListOfMessages } from 'app/features/messages/hooks/use-message-editor';
|
||||
import { getMessage } from 'app/features/messages/hooks/use-message';
|
||||
import messageApiClient from 'app/features/messages/api/message-api-client';
|
||||
import User from 'app/features/users/services/current-user-service';
|
||||
import { useChannelMembersReadSections } from 'app/features/channel-members/hooks/use-channel-members-read-sections';
|
||||
import { delayRequest } from 'app/features/global/utils/managedSearchRequest';
|
||||
import { useRefreshPublicOrPrivateChannels } from 'app/features/channels/hooks/use-public-or-private-channels';
|
||||
import { usePageVisibility } from "react-page-visibility";
|
||||
|
||||
type Props = {
|
||||
companyId: string;
|
||||
workspaceId?: string;
|
||||
channelId?: string;
|
||||
threadId?: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const MessagesListContext = React.createContext({
|
||||
hideReplies: false,
|
||||
withBlock: false,
|
||||
readonly: false,
|
||||
});
|
||||
|
||||
export default ({ channelId, companyId, workspaceId, readonly }: Props) => {
|
||||
const listBuilderRef = useRef<ListBuilderHandle>(null);
|
||||
const [atBottom, setAtBottom] = useState(true);
|
||||
const { seen, refresh: loadReadSections } = useChannelMembersReadSections(
|
||||
companyId,
|
||||
workspaceId || 'direct',
|
||||
channelId || '',
|
||||
);
|
||||
|
||||
const {
|
||||
messages: _messages,
|
||||
loadMore,
|
||||
window,
|
||||
jumpTo,
|
||||
convertToKeys,
|
||||
} = useChannelMessages({
|
||||
companyId,
|
||||
workspaceId: workspaceId || '',
|
||||
channelId: channelId || '',
|
||||
});
|
||||
let messages = _messages;
|
||||
const { company } = useCurrentCompany();
|
||||
const shouldLimit = MessageHistoryService.shouldLimitMessages(
|
||||
company,
|
||||
messages[0]?.id || '',
|
||||
messages.length,
|
||||
);
|
||||
messages = withNonMessagesComponents(messages, window.reachedStart, shouldLimit);
|
||||
|
||||
const loadMoreMessages = async (
|
||||
direction: 'history' | 'future',
|
||||
limit?: number,
|
||||
offsetItem?: MessagesAndComponentsType,
|
||||
) => {
|
||||
const messages = await loadMore(direction, limit, offsetItem?.threadId);
|
||||
return withNonMessagesComponents(
|
||||
convertToKeys(company.id, messages),
|
||||
window.reachedStart && direction === 'history',
|
||||
shouldLimit && direction === 'history',
|
||||
);
|
||||
};
|
||||
|
||||
const { refresh: refreshChannels } = useRefreshPublicOrPrivateChannels();
|
||||
const isPageVisible = usePageVisibility();
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) loadMore('history');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.reachedEnd && atBottom && messages.length > 0 && isPageVisible) {
|
||||
const seenMessages = messages.filter(message => {
|
||||
const m = getMessage(message.id || message.threadId);
|
||||
const currentUserId = User.getCurrentUserId();
|
||||
|
||||
if (m.user_id === currentUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m.status === 'delivered' || (m.status === 'read' && !seen(currentUserId, m.id));
|
||||
});
|
||||
if (seenMessages.length > 0) {
|
||||
delayRequest('message-list-read-request', async () => {
|
||||
await messageApiClient.read(
|
||||
companyId,
|
||||
channelId || '',
|
||||
workspaceId || 'direct',
|
||||
seenMessages,
|
||||
);
|
||||
await loadReadSections();
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [messages, messages.length, window.reachedEnd, isPageVisible]);
|
||||
|
||||
const { highlight, cancelHighlight, reachedHighlight } = useHighlightMessage();
|
||||
|
||||
useEffect(() => {
|
||||
//Manage scroll to highlight
|
||||
if (listBuilderRef.current && highlight && !highlight.reachedThread) {
|
||||
if (highlight.answerId) {
|
||||
SideViewService.select(channelId || '', {
|
||||
app: { identity: { code: 'messages' } } as Application,
|
||||
context: {
|
||||
viewType: 'channel_thread',
|
||||
threadId: highlight.threadId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Find the correct index of required message
|
||||
const index = messages.findIndex(m => m.id === highlight.threadId);
|
||||
if (index < 0) {
|
||||
// Load the right portion of messages
|
||||
jumpTo(highlight.threadId);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (listBuilderRef.current)
|
||||
listBuilderRef.current.scrollToIndex({
|
||||
align: 'start',
|
||||
index: index,
|
||||
});
|
||||
setTimeout(() => {
|
||||
//Need to wait a bit for the scroll to ends
|
||||
reachedHighlight('thread');
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
}
|
||||
}, [highlight, messages.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length) {
|
||||
ChannelAPIClient.read(companyId, workspaceId || '', channelId || '', { status: true }).then(
|
||||
() => {
|
||||
refreshChannels();
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [messages.length > 0]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length) {
|
||||
delayRequest('message-list-load-read-sections', async () => {
|
||||
loadReadSections();
|
||||
});
|
||||
}
|
||||
}, [companyId, workspaceId, channelId, messages.length > 0]);
|
||||
|
||||
const row = React.useMemo(
|
||||
() => (_: number, m: MessagesAndComponentsType) => {
|
||||
if (m.type === 'timeseparator') {
|
||||
return (
|
||||
<div key={m.type + m.threadId}>
|
||||
<TimeSeparator date={m.date || 0} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (m.type === 'header') {
|
||||
return (
|
||||
<div key={m.type + m.threadId}>
|
||||
<FirstMessage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (m.type === 'locked') {
|
||||
return (
|
||||
<div key={m.type + m.threadId}>
|
||||
<LockedHistoryBanner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={m.type + m.threadId}>
|
||||
<MessageWithReplies
|
||||
companyId={m.companyId}
|
||||
workspaceId={workspaceId || ''}
|
||||
channelId={channelId || ''}
|
||||
threadId={m.threadId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const jumpToBottom = () => {
|
||||
if (window.reachedEnd) {
|
||||
listBuilderRef.current?.scrollTo({ top: 10000000, behavior: 'smooth' });
|
||||
} else {
|
||||
// Load the right portion of messages
|
||||
jumpTo('');
|
||||
}
|
||||
};
|
||||
|
||||
//This hide virtuoso but it start to work in backend
|
||||
const virtuosoLoading = highlight && !highlight?.reachedThread;
|
||||
|
||||
return (
|
||||
<MessagesListContext.Provider
|
||||
value={{ hideReplies: false, withBlock: true, readonly: !!readonly }}
|
||||
>
|
||||
{(!window.loaded || virtuosoLoading) && <MessagesPlaceholder />}
|
||||
{!window.loaded && <div style={{ flex: 1 }}></div>}
|
||||
{window.loaded && (
|
||||
<ListBuilder
|
||||
ref={listBuilderRef}
|
||||
style={virtuosoLoading ? { opacity: 0 } : {}}
|
||||
onScroll={(e: React.UIEvent<'div', UIEvent>) => {
|
||||
const scrollBottom =
|
||||
(e.target as HTMLElement).scrollHeight -
|
||||
(e.target as HTMLElement).scrollTop -
|
||||
(e.target as HTMLElement).clientHeight;
|
||||
const closeToBottom = scrollBottom < 500;
|
||||
if (closeToBottom !== atBottom) setAtBottom(closeToBottom);
|
||||
cancelHighlight();
|
||||
}}
|
||||
items={messages}
|
||||
filterOnAppend={messages => {
|
||||
return cleanFrontMessagesFromListOfMessages(messages);
|
||||
}}
|
||||
itemId={m => m.type + (getMessage(m.id)?.context?._front_id || m.threadId) + m.id}
|
||||
emptyListComponent={<FirstMessage />}
|
||||
itemContent={row}
|
||||
followOutput={!!window.reachedEnd && 'smooth'}
|
||||
loadMore={loadMoreMessages}
|
||||
atBottomStateChange={async (atBottom: boolean) => {
|
||||
if (atBottom && window.reachedEnd) {
|
||||
setAtBottom(true);
|
||||
await ChannelAPIClient.read(companyId, workspaceId || '', channelId || '', {
|
||||
status: true,
|
||||
});
|
||||
refreshChannels();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!(atBottom && window.reachedEnd) && window.loaded && messages.length > 0 && (
|
||||
<GoToBottom
|
||||
onClick={() => {
|
||||
jumpToBottom();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MessagesListContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,281 +0,0 @@
|
||||
.messages-view {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.loading-blink {
|
||||
@keyframes loading-blink {
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
20%,
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
animation: loading-blink 1s infinite;
|
||||
}
|
||||
|
||||
.go-to-bottom {
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
bottom: 46px;
|
||||
margin-bottom: -32px;
|
||||
height: 28px;
|
||||
width: fit-content;
|
||||
z-index: 4;
|
||||
font-size: 14px;
|
||||
color: var(--white);
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
background-color: var(--primary);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.user-writing-info-message-view {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
min-width: 0;
|
||||
max-width: 1000px;
|
||||
width: calc(100% - 32px);
|
||||
position: relative;
|
||||
|
||||
.user-writing-info-message-view-inner {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
color: var(--grey-dark);
|
||||
bottom: -8px;
|
||||
left: 0px;
|
||||
font-size: 11px;
|
||||
pointer-events: none;
|
||||
background: #ffffff88;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-full {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
z-index: 1;
|
||||
.loading {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.loader {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.loader {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.scrolled-up) {
|
||||
.go-to-bottom {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.messages-scroller-parent {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
background: var(--white);
|
||||
border-top: 1px solid transparent;
|
||||
z-index: 4;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
transition: height 0.2s;
|
||||
}
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 0 -16px 8px -8px rgba(255, 255, 255, 0) inset;
|
||||
z-index: 4;
|
||||
bottom: 8px;
|
||||
pointer-events: none;
|
||||
transition: box-shadow 0.2s, bottom 0.2s;
|
||||
}
|
||||
&.scrolled-up-100 {
|
||||
&:after,
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
&:before {
|
||||
border-top: 1px solid var(--grey-background);
|
||||
height: 16px;
|
||||
}
|
||||
&:after {
|
||||
bottom: 16px;
|
||||
box-shadow: 0 -32px 32px -32px rgba(0, 0, 0, 0.1) inset;
|
||||
}
|
||||
}
|
||||
&.scrolled-up {
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 0 -16px 8px -8px rgba(255, 255, 255, 1) inset;
|
||||
z-index: 4;
|
||||
bottom: 0px;
|
||||
pointer-events: none;
|
||||
transition: box-shadow 0.2s, bottom 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.message_header {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.thread .message_timeline {
|
||||
text-align: left;
|
||||
background: var(--white);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
.time_container {
|
||||
margin-left: 32px;
|
||||
top: -12px;
|
||||
}
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.time_separator {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.message_timeline {
|
||||
text-align: center;
|
||||
color: var(--grey-dark);
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
height: 22px;
|
||||
margin: 0;
|
||||
padding-bottom: 0px;
|
||||
padding-top: 0px;
|
||||
min-width: 0;
|
||||
max-width: 1000px;
|
||||
width: calc(100% - 32px);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--grey-background);
|
||||
position: relative;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.new_messages {
|
||||
&::before {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.time_container {
|
||||
.time {
|
||||
font-weight: 700;
|
||||
background: var(--white);
|
||||
color: var(--primary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time_container {
|
||||
display: inline-block;
|
||||
padding: 0px 11px;
|
||||
background: var(--white);
|
||||
top: -15px;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
|
||||
.time {
|
||||
color: var(--grey-dark);
|
||||
width: auto;
|
||||
z-index: 1;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
margin: auto;
|
||||
font-size: 12px;
|
||||
border-radius: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.writing_message {
|
||||
font-size: 12px;
|
||||
color: var(--grey-dark);
|
||||
font-size: 12px;
|
||||
padding-bottom: 8px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
right: 0;
|
||||
width: calc(100% - 48px);
|
||||
background: #fff;
|
||||
transition: opacity 0.2s;
|
||||
min-width: 0;
|
||||
max-width: 1000px;
|
||||
width: calc(100% - 32px);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import React, { Suspense, useState } from 'react';
|
||||
import { ChannelType } from 'app/features/channels/types/channel';
|
||||
import { ViewConfiguration } from 'app/features/router/services/app-view-service';
|
||||
import NewThread from './input/new-thread';
|
||||
import MessagesList from './messages-list';
|
||||
import ThreadMessagesList from './thread-messages-list';
|
||||
import IsWriting from './input/parts/IsWriting';
|
||||
import {
|
||||
useChannel,
|
||||
useIsChannelMember,
|
||||
useIsReadOnlyChannel,
|
||||
} from 'app/features/channels/hooks/use-channel';
|
||||
import { Button } from 'app/atoms/button/button';
|
||||
import ChannelsReachableAPIClient from 'app/features/channels/api/channels-reachable-api-client';
|
||||
import UserService from 'app/features/users/services/current-user-service';
|
||||
import * as Text from '@atoms/text';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import MessageSeenBy from 'app/components/message-seen-by/message-seen-by';
|
||||
import { useUser } from 'app/features/users/hooks/use-user';
|
||||
import { UserType } from 'app/features/users/types/user';
|
||||
import { ForwardMessageModal } from 'app/components/forward-message';
|
||||
import AccessRightsService from 'app/features/workspace-members/services/workspace-members-access-rights-service';
|
||||
|
||||
type Props = {
|
||||
channel: ChannelType;
|
||||
tab?: unknown;
|
||||
options: ViewConfiguration;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
if (!props.channel) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const companyId = props.channel.company_id || '';
|
||||
const workspaceId = props.channel.workspace_id || '';
|
||||
const channelId = props.channel.id || '';
|
||||
const isDirectChannel = props.channel.visibility !== 'direct';
|
||||
const threadId = props.options.context?.threadId || '';
|
||||
const isChannelMember = useIsChannelMember(channelId);
|
||||
const currentUser = UserService.getCurrentUser();
|
||||
let userIsNotInCompany = false;
|
||||
const otherChannelsMembersThanMe =
|
||||
(props.channel.members || []).filter(id => id !== currentUser?.id) || [];
|
||||
const otherUserThatIsNotMe = useUser(otherChannelsMembersThanMe[0] || '');
|
||||
if (
|
||||
otherUserThatIsNotMe &&
|
||||
otherChannelsMembersThanMe.length === 1 &&
|
||||
!UserService.isInCompany(otherUserThatIsNotMe as UserType, companyId)
|
||||
) {
|
||||
userIsNotInCompany = true;
|
||||
}
|
||||
|
||||
const channelIsRestricted =
|
||||
useIsReadOnlyChannel(channelId) &&
|
||||
currentUser.id !== props.channel.owner &&
|
||||
!AccessRightsService.hasLevel(workspaceId, 'moderator');
|
||||
|
||||
return (
|
||||
<div className="messages-view">
|
||||
<ForwardMessageModal />
|
||||
|
||||
<Suspense fallback={<></>}>
|
||||
{!threadId ? (
|
||||
<MessagesList
|
||||
key={channelId + threadId}
|
||||
companyId={companyId}
|
||||
workspaceId={workspaceId}
|
||||
channelId={channelId}
|
||||
threadId={threadId}
|
||||
readonly={userIsNotInCompany}
|
||||
/>
|
||||
) : (
|
||||
<ThreadMessagesList
|
||||
key={channelId + threadId}
|
||||
companyId={companyId}
|
||||
workspaceId={workspaceId}
|
||||
channelId={channelId}
|
||||
threadId={threadId}
|
||||
readonly={userIsNotInCompany}
|
||||
/>
|
||||
)}{' '}
|
||||
<MessageSeenBy />
|
||||
</Suspense>
|
||||
<IsWriting channelId={channelId} threadId={threadId} />
|
||||
{isChannelMember && channelIsRestricted && <ChannelIsRestricted />}
|
||||
{isChannelMember && !channelIsRestricted && !userIsNotInCompany && (
|
||||
<NewThread
|
||||
collectionKey=""
|
||||
useButton={isDirectChannel && !threadId}
|
||||
channelId={channelId}
|
||||
threadId={threadId}
|
||||
/>
|
||||
)}
|
||||
{isChannelMember && userIsNotInCompany && <UserIsNotInCompany />}
|
||||
{!isChannelMember && <JoinChanneBlock channelId={channelId} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const JoinChanneBlock = ({ channelId }: { channelId: string }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { channel, refresh } = useChannel(channelId);
|
||||
|
||||
if (!channel) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-t border-zinc-200 dark:border-zinc-700 p-8 text-center">
|
||||
<Button
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
await ChannelsReachableAPIClient.inviteUser(
|
||||
channel.company_id || '',
|
||||
channel.workspace_id || '',
|
||||
channel.id || '',
|
||||
UserService.getCurrentUserId(),
|
||||
);
|
||||
refresh();
|
||||
setLoading(false);
|
||||
}}
|
||||
className="mb-4"
|
||||
>
|
||||
{Languages.t('scenes.client.join_public_channel')}
|
||||
</Button>
|
||||
<br />
|
||||
<Text.Info>{Languages.t('scenes.client.join_public_channel.info')}</Text.Info>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UserIsNotInCompany = () => {
|
||||
return (
|
||||
<div className="border-t border-zinc-200 dark:border-zinc-700 p-8 text-center">
|
||||
<Text.Info>{Languages.t('scenes.apps.messages.message.user_deactivated')}</Text.Info>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChannelIsRestricted = () => {
|
||||
return (
|
||||
<div className="border-t border-zinc-200 dark:border-zinc-700 p-8 text-center">
|
||||
<Text.Info>{Languages.t('scenes.client.readonly.info')}</Text.Info>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { ArrowDown } from 'react-feather';
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
newMessages?: number;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const msg = props.newMessages
|
||||
? Languages.t('scenes.apps.messages.messageslist.go_last_message_button.new_messages')
|
||||
: Languages.t('scenes.apps.messages.messageslist.go_last_message_button');
|
||||
|
||||
return (
|
||||
<div className={'go-to-bottom'} key="go-to-bottom" onClick={() => props.onClick()}>
|
||||
<ArrowDown size={16} />
|
||||
<span>{msg}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import 'moment-timezone';
|
||||
import User from 'app/features/users/services/current-user-service';
|
||||
import WorkspacesApps from 'app/deprecated/workspaces/workspaces_apps.jsx';
|
||||
import Icon from 'app/components/icon/icon.jsx';
|
||||
import './threads.scss';
|
||||
import { MessageContext } from '../message/message-with-replies';
|
||||
import { useMessage } from 'app/features/messages/hooks/use-message';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { CompanyApplicationsStateFamily } from 'app/features/applications/state/company-applications';
|
||||
import { useUser } from 'app/features/users/hooks/use-user';
|
||||
|
||||
type Props = {
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
const context = useContext(MessageContext);
|
||||
const { message } = useMessage(context);
|
||||
|
||||
const user =
|
||||
useUser(message.user_id) || (message.users || []).find(u => u.id === message.user_id);
|
||||
const companyApplications =
|
||||
useRecoilState(CompanyApplicationsStateFamily(context.companyId))[0] || [];
|
||||
const application = companyApplications.find(a => a.id === message.application_id);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!user && !message.application_id && (
|
||||
<div
|
||||
className="sender-head"
|
||||
style={{
|
||||
backgroundImage: "url('" + User.getThumbnail(user) + "')",
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{!!message.application_id && (
|
||||
<Icon
|
||||
className="no-margin-left"
|
||||
style={{ fontSize: props.small ? '16px' : '24px' }}
|
||||
type={
|
||||
message.override?.picture || WorkspacesApps.getAppIcon(application) || 'puzzle-piece'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import React, { ReactNode, Suspense } from 'react';
|
||||
import 'moment-timezone';
|
||||
import './threads.scss';
|
||||
import ThreadAvatar from './thread-avatar';
|
||||
|
||||
type Props = {
|
||||
compact?: boolean;
|
||||
gradient?: boolean;
|
||||
small?: boolean;
|
||||
head?: boolean;
|
||||
alinea?: boolean;
|
||||
children?: ReactNode;
|
||||
noSenderSpace?: boolean;
|
||||
withAvatar?: boolean;
|
||||
pinned?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export default (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
onClick={props.onClick}
|
||||
className={
|
||||
'thread-section ' +
|
||||
(props.compact ? 'compact ' : '') +
|
||||
(props.gradient ? 'gradient ' : '') +
|
||||
(props.small ? 'small-section ' : '') +
|
||||
(props.alinea ? 'alinea ' : '') +
|
||||
(props.head ? 'head-section ' : '') +
|
||||
(props.pinned ? 'pinned-section ' : '') +
|
||||
(props.className ? props.className + ' ' : '')
|
||||
}
|
||||
>
|
||||
<div className="message">
|
||||
{!props.noSenderSpace && (
|
||||
<div className="sender-space">
|
||||
{props.withAvatar && (
|
||||
<Suspense fallback={''}>
|
||||
<ThreadAvatar small={props.small} />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import './threads.scss';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const LoadingThread = (
|
||||
<div className="thread-section">
|
||||
<div className="message">
|
||||
<div className="sender-space">
|
||||
<div className="sender-head" />
|
||||
</div>
|
||||
<div className="message-content">
|
||||
<div className="message-content-header">
|
||||
<span className="sender-name"></span>
|
||||
</div>
|
||||
<div className="content-parent"></div>
|
||||
<div className="content-parent" style={{ width: '40%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
hidden?: boolean;
|
||||
highlighted?: boolean;
|
||||
withBlock?: boolean;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default ({ hidden, highlighted, className, withBlock, children }: Props) => (
|
||||
<div className={classNames('thread-container', { hidden, highlighted }, className)}>
|
||||
<div className="thread-centerer">
|
||||
<div className={classNames('thread', { 'with-block': withBlock })}>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,207 +0,0 @@
|
||||
.dragged {
|
||||
.draggable_clone > .thread,
|
||||
.draggable_clone > .thread-section {
|
||||
opacity: 1;
|
||||
border: 1px solid var(--grey-background);
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--border-radius-base);
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
|
||||
.message {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sender-space {
|
||||
width: 28px;
|
||||
}
|
||||
}
|
||||
.draggable_clone > .thread-section,
|
||||
.draggable_clone > .thread .head-section {
|
||||
.message-options.drag {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drag_message {
|
||||
.thread-container.has-droppable {
|
||||
.thread:hover:not(.dragging_opacity) {
|
||||
border: 1px solid var(--primary) !important;
|
||||
box-shadow: 0 2px 4px 0 var(--primary-background) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.message-options {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.thread-container,
|
||||
.dragged {
|
||||
.thread.dragging_opacity > *,
|
||||
.thread-section.dragging_opacity > * {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&.highlighted .thread,
|
||||
&.highlighted .thread.with-block {
|
||||
border-radius: var(--border-radius-base);
|
||||
box-shadow: 0 0 0 4px #fff892;
|
||||
.thread-section .message {
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
}
|
||||
opacity: 1;
|
||||
transition: background-color 0.2s, box-shadow 0.2s;
|
||||
min-width: 0;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.thread-centerer {
|
||||
margin: 0 auto;
|
||||
min-width: 0;
|
||||
max-width: 1000px;
|
||||
width: calc(100% - 32px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thread {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&.with-block {
|
||||
border: 1px solid var(--grey-background);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--border-radius-base);
|
||||
// in infinite list, margin are bad to compute render size because they are hard to measure
|
||||
// using padding solves this issue anbd allow to scroll correctly
|
||||
// cf https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around
|
||||
margin-bottom: 0px;
|
||||
background: var(--white);
|
||||
|
||||
.message {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thread-section {
|
||||
position: relative;
|
||||
&.gradient {
|
||||
margin-bottom: -40px;
|
||||
height: 64px;
|
||||
background: linear-gradient(180deg, #fff 40%, #ffffff00);
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
border: none;
|
||||
.message {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child):not(.gradient) {
|
||||
border-bottom: 1px solid var(--grey-background);
|
||||
}
|
||||
|
||||
&.compact {
|
||||
.message {
|
||||
padding: 4px 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
border-left: 4px solid transparent;
|
||||
background-color: var(--white);
|
||||
padding: 4px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
border-radius: var(--border-radius-base) 0 0 0;
|
||||
}
|
||||
|
||||
&.small-section {
|
||||
.message .sender-space {
|
||||
.sender-head,
|
||||
.app_icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
|
||||
.online_user_status {
|
||||
position: relative;
|
||||
bottom: -12px;
|
||||
right: -12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.alinea {
|
||||
.message {
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&.pinned-section {
|
||||
.message {
|
||||
border-left: 4px solid var(--primary);
|
||||
box-shadow: -8px 0px 12px -8px var(--primary-background);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.loading {
|
||||
.sender-head,
|
||||
.app_icon,
|
||||
.sender-name,
|
||||
.content-parent {
|
||||
animation-duration: 10s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: placeHolderShimmer;
|
||||
animation-timing-function: linear;
|
||||
background-repeat: repeat !important;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--grey-light) 0px,
|
||||
#f2f2f2 120px,
|
||||
var(--grey-light) 234px
|
||||
) !important;
|
||||
}
|
||||
.sender-name {
|
||||
width: 30%;
|
||||
max-width: 200px;
|
||||
}
|
||||
.content-parent {
|
||||
width: 100%;
|
||||
}
|
||||
.sender-name,
|
||||
.content-parent {
|
||||
height: 14px;
|
||||
margin: 4px 0;
|
||||
display: inline-block;
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
div,
|
||||
span {
|
||||
font-size: 0px !important;
|
||||
}
|
||||
|
||||
.thread-section {
|
||||
border: 0px !important;
|
||||
}
|
||||
|
||||
.thread-section.compact {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Skeleton } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
export const MessagesPlaceholder = () => {
|
||||
return (
|
||||
<div
|
||||
className="messages-placeholder thread-container loading-blink"
|
||||
style={{ flex: 1, overflow: 'hidden', bottom: '0px', position: 'absolute', width: '100%' }}
|
||||
>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => (
|
||||
<div key={i} className="thread-centerer">
|
||||
<Skeleton avatar></Skeleton>
|
||||
</div>
|
||||
))}{' '}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,166 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ListBuilder, { ListBuilderHandle } from './list-builder';
|
||||
import TimeSeparator from './message/time-separator';
|
||||
import MessageWithReplies from './message/message-with-replies';
|
||||
import FirstThreadMessage from './message/parts/FirstMessage/FirstThreadMessage';
|
||||
import { MessagesListContext } from './messages-list';
|
||||
import { useThreadMessages } from 'app/features/messages/hooks/use-thread-messages';
|
||||
import {
|
||||
MessagesAndComponentsType,
|
||||
withNonMessagesComponents,
|
||||
} from '../../../features/messages/hooks/with-non-messages-components';
|
||||
import { useHighlightMessage } from 'app/features/messages/hooks/use-highlight-message';
|
||||
import GoToBottom from './parts/go-to-bottom';
|
||||
import { MessagesPlaceholder } from './placeholder';
|
||||
import { cleanFrontMessagesFromListOfMessages } from 'app/features/messages/hooks/use-message-editor';
|
||||
import { getMessage } from 'app/features/messages/hooks/use-message';
|
||||
|
||||
type Props = {
|
||||
companyId: string;
|
||||
workspaceId: string;
|
||||
channelId: string;
|
||||
threadId: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export default ({ companyId, workspaceId, channelId, threadId, readonly }: Props) => {
|
||||
const listBuilderRef = useRef<ListBuilderHandle>(null);
|
||||
const [atBottom, setAtBottom] = useState(true);
|
||||
|
||||
const { highlight, cancelHighlight, reachedHighlight } = useHighlightMessage();
|
||||
const {
|
||||
messages: _messages,
|
||||
loadMore,
|
||||
window,
|
||||
jumpTo,
|
||||
convertToKeys,
|
||||
} = useThreadMessages({
|
||||
companyId,
|
||||
threadId: threadId || '',
|
||||
});
|
||||
const messages = withNonMessagesComponents(_messages, window.reachedStart);
|
||||
|
||||
useEffect(() => {
|
||||
loadMore('history');
|
||||
}, []);
|
||||
|
||||
const loadMoreMessages = async (
|
||||
direction: 'history' | 'future',
|
||||
limit?: number,
|
||||
offsetItem?: MessagesAndComponentsType,
|
||||
) => {
|
||||
const messages = await loadMore(direction, limit, offsetItem?.id);
|
||||
return withNonMessagesComponents(
|
||||
convertToKeys(companyId, messages),
|
||||
window.reachedStart && direction === 'history',
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
//Manage scroll to highlight
|
||||
if (
|
||||
listBuilderRef.current &&
|
||||
highlight &&
|
||||
highlight.answerId &&
|
||||
highlight.reachedThread &&
|
||||
!highlight.reachedAnswer
|
||||
) {
|
||||
// Find the correct index of required message
|
||||
const index = messages.findIndex(m => m.id === highlight.answerId);
|
||||
if (index < 0) {
|
||||
// Load the right portion of messages
|
||||
jumpTo(highlight.answerId);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (listBuilderRef.current)
|
||||
listBuilderRef.current.scrollToIndex({
|
||||
align: 'start',
|
||||
index: index,
|
||||
});
|
||||
setTimeout(() => reachedHighlight('answer'), 1000);
|
||||
}, 1000);
|
||||
}
|
||||
}, [highlight, messages.length]);
|
||||
|
||||
const jumpToBottom = () => {
|
||||
if (window.reachedEnd) {
|
||||
listBuilderRef.current?.scrollTo({ top: 10000000, behavior: 'smooth' });
|
||||
} else {
|
||||
// Load the right portion of messages
|
||||
jumpTo('');
|
||||
}
|
||||
};
|
||||
|
||||
//This hide virtuoso but it start to work in backend
|
||||
const virtuosoLoading = highlight && highlight.answerId && !highlight?.reachedAnswer;
|
||||
|
||||
return (
|
||||
<MessagesListContext.Provider
|
||||
value={{ hideReplies: true, withBlock: false, readonly: !!readonly }}
|
||||
>
|
||||
{(!window.loaded || virtuosoLoading) && <MessagesPlaceholder />}
|
||||
{!window.loaded && <div style={{ flex: 1 }}></div>}
|
||||
{window.loaded && (
|
||||
<ListBuilder
|
||||
key={threadId}
|
||||
followOutput={!!window.reachedEnd && 'smooth'}
|
||||
ref={listBuilderRef}
|
||||
style={virtuosoLoading ? { opacity: 0 } : {}}
|
||||
onScroll={(e: React.UIEvent<'div', UIEvent>) => {
|
||||
const scrollBottom =
|
||||
(e.target as HTMLElement).scrollHeight -
|
||||
(e.target as HTMLElement).scrollTop -
|
||||
(e.target as HTMLElement).clientHeight;
|
||||
const closeToBottom = scrollBottom < 100;
|
||||
if (closeToBottom !== atBottom) setAtBottom(closeToBottom);
|
||||
cancelHighlight();
|
||||
}}
|
||||
items={messages}
|
||||
itemId={m => m.type + (getMessage(m.id)?.context?._front_id || m.id) + m.id}
|
||||
emptyListComponent={<FirstThreadMessage noReplies />}
|
||||
filterOnAppend={messages => {
|
||||
return cleanFrontMessagesFromListOfMessages(messages);
|
||||
}}
|
||||
itemContent={(_index, m) => {
|
||||
if (m.type === 'timeseparator') {
|
||||
return (
|
||||
<div key={m.type + m.id}>
|
||||
<TimeSeparator date={m.date || 0} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (m.type === 'header') {
|
||||
return (
|
||||
<div key={m.type + m.threadId}>
|
||||
<FirstThreadMessage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={m.type + m.id}>
|
||||
<MessageWithReplies
|
||||
companyId={m.companyId}
|
||||
workspaceId={workspaceId || ''}
|
||||
channelId={channelId || ''}
|
||||
threadId={m.threadId}
|
||||
id={m.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
loadMore={loadMoreMessages}
|
||||
/>
|
||||
)}
|
||||
{!(atBottom && window.reachedEnd) && window.loaded && messages.length > 0 && (
|
||||
<GoToBottom
|
||||
onClick={() => {
|
||||
jumpToBottom();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</MessagesListContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import React, { Component } from 'react';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import Workspaces from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
import Emojione from 'components/emojione/emojione';
|
||||
import Loader from 'components/loader/loader.jsx';
|
||||
|
||||
export default class BoardPicker extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
i18n: Languages,
|
||||
};
|
||||
Languages.addListener(this);
|
||||
|
||||
this.boards_collection_key = 'boards_picker_' + Workspaces.getCurrentUserId;
|
||||
|
||||
Collections.get('boards').addListener(this);
|
||||
Collections.get('boards').addSource(
|
||||
{
|
||||
http_base_url: 'tasks/board',
|
||||
http_options: {
|
||||
workspace_id: Workspaces.getCurrentUserId,
|
||||
},
|
||||
websockets: [{ uri: 'boards/' + Workspaces.getCurrentUserId, options: { type: 'board' } }],
|
||||
},
|
||||
this.boards_collection_key,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
Languages.removeListener(this);
|
||||
Collections.get('boards').removeSource(this.boards_collection_key);
|
||||
}
|
||||
render() {
|
||||
var boards = Collections.get('boards').findBy({ workspace_id: Workspaces.currentWorkspaceId });
|
||||
var loading =
|
||||
!Collections.get('boards').did_load_first_time[this.boards_collection_key] &&
|
||||
boards.length == 0;
|
||||
|
||||
return (
|
||||
<div className="boardPicker">
|
||||
{loading && (
|
||||
<div className="loading">
|
||||
<Loader color="#CCC" className="app_loader" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
boards.map(board => {
|
||||
return (
|
||||
<div
|
||||
className="board_frame fade_in"
|
||||
onClick={() => {
|
||||
this.props.onChoose(board);
|
||||
}}
|
||||
>
|
||||
<div className="board_name app_title">
|
||||
{board.emoji && <Emojione type={board.emoji} s32 className="board_emoji" />}
|
||||
{board.title}
|
||||
</div>
|
||||
<div className="board_info">
|
||||
{board.active_tasks || '0'}{' '}
|
||||
{Languages.t('scenes.apps.tasks.active_tasks', [], 'tâches actives')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,501 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React from 'react';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import List from './list/list.jsx';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import Loader from 'components/loader/loader.jsx';
|
||||
import './Board.scss';
|
||||
import Emojione from 'components/emojione/emojione';
|
||||
import Rounded from 'components/inputs/rounded.jsx';
|
||||
import Menu from 'components/menus/menu.jsx';
|
||||
import ListEditor from './list/list-editor.jsx';
|
||||
import TasksService from 'app/deprecated/Apps/Tasks/Tasks.js';
|
||||
import Tabs from 'components/tabs/tabs.jsx';
|
||||
import ChevronDownIcon from '@material-ui/icons/KeyboardArrowDownOutlined';
|
||||
import User from 'components/user/user.jsx';
|
||||
import MenusManager from 'app/components/menus/menus-manager.jsx';
|
||||
import RouterService from 'app/features/router/services/router-service';
|
||||
|
||||
export default class Board extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.props = props;
|
||||
this.state = {
|
||||
i18n: Languages,
|
||||
archived: false,
|
||||
};
|
||||
Languages.addListener(this);
|
||||
|
||||
this.board_collection_key = 'board_' + this.props.board.id;
|
||||
|
||||
this.user_mode = this.props.board.id.split('_')[0] === 'user';
|
||||
|
||||
const { channelId } = RouterService.getStateFromRoute();
|
||||
|
||||
//Lists (only in board mode, not in user mode)
|
||||
if (!this.user_mode) {
|
||||
Collections.get('lists').addListener(this);
|
||||
Collections.get('lists').addSource(
|
||||
{
|
||||
http_base_url: 'tasks/list',
|
||||
http_options: {
|
||||
channel_id: channelId,
|
||||
board_id: this.props.board.id,
|
||||
},
|
||||
websockets: [{ uri: 'board_lists/' + this.props.board.id, options: { type: 'list' } }],
|
||||
},
|
||||
this.board_collection_key,
|
||||
);
|
||||
}
|
||||
|
||||
Collections.get('tasks').addListener(this);
|
||||
Collections.get('tasks').addSource(
|
||||
{
|
||||
http_base_url: 'tasks/task',
|
||||
http_options: {
|
||||
channel_id: channelId,
|
||||
board_id: this.props.board.id,
|
||||
},
|
||||
websockets: [{ uri: 'board_tasks/' + this.props.board.id, options: { type: 'task' } }],
|
||||
},
|
||||
this.board_collection_key,
|
||||
);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
Languages.removeListener(this);
|
||||
|
||||
if (!this.user_mode) {
|
||||
Collections.get('lists').removeListener(this);
|
||||
Collections.get('lists').removeSource(this.board_collection_key);
|
||||
}
|
||||
|
||||
Collections.get('tasks').removeListener(this);
|
||||
Collections.get('tasks').removeSource(this.board_collection_key);
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
if (this.retry_update) clearTimeout(this.retry_update);
|
||||
if (TasksService.paused_notify[this.props.board.id]) {
|
||||
this.retry_update = setTimeout(() => {
|
||||
this.setState({});
|
||||
}, 1000);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onDragStart = () => {
|
||||
TasksService.paused_notify[this.props.board.id] = true;
|
||||
};
|
||||
|
||||
onDragEnd = event => {
|
||||
TasksService.paused_notify[this.props.board.id] = false;
|
||||
|
||||
if (!event.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'board') {
|
||||
var element_front_id = event.draggableId.split('_')[1];
|
||||
var new_index = event.destination.index;
|
||||
var list = Collections.get('lists').findByFrontId(element_front_id);
|
||||
if (list) {
|
||||
Collections.get('lists').updateObject(
|
||||
{
|
||||
order: TasksService.newIndexAfter(
|
||||
'lists_' + this.props.board.id,
|
||||
new_index - (new_index < event.source.index ? 1 : 0),
|
||||
),
|
||||
},
|
||||
list.front_id,
|
||||
);
|
||||
TasksService.setElementIndexPool(
|
||||
'lists_' + this.props.board.id,
|
||||
Collections.get('lists').findBy({ board_id: this.props.board.id }),
|
||||
);
|
||||
Collections.get('lists').save(list, this.board_collection_key);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'list') {
|
||||
// eslint-disable-next-line no-redeclare
|
||||
var element_front_id = event.draggableId.split('_')[1];
|
||||
var source_list_front_id = event.source.droppableId;
|
||||
var destination_list_front_id = event.destination.droppableId;
|
||||
var destination_index = event.destination.index;
|
||||
|
||||
var source_list = Collections.get('lists').findByFrontId(source_list_front_id);
|
||||
// eslint-disable-next-line no-redeclare
|
||||
var list = Collections.get('lists').findByFrontId(destination_list_front_id);
|
||||
var task = Collections.get('tasks').findByFrontId(element_front_id);
|
||||
|
||||
if (task && list && list.id) {
|
||||
Collections.get('tasks').updateObject(
|
||||
{
|
||||
list_id: list.id,
|
||||
order: TasksService.newIndexAfter(
|
||||
'tasks_' + list.id,
|
||||
destination_index -
|
||||
(destination_index < event.source.index ||
|
||||
destination_list_front_id !== source_list_front_id
|
||||
? 1
|
||||
: 0),
|
||||
),
|
||||
},
|
||||
task.front_id,
|
||||
);
|
||||
if (source_list && source_list.id) {
|
||||
TasksService.setElementIndexPool(
|
||||
'tasks_' + source_list.id,
|
||||
Collections.get('tasks').findBy({
|
||||
board_id: this.props.board.id,
|
||||
list_id: source_list.id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
TasksService.setElementIndexPool(
|
||||
'tasks_' + list.id,
|
||||
Collections.get('tasks').findBy({ board_id: this.props.board.id, list_id: list.id }),
|
||||
);
|
||||
Collections.get('tasks').save(task, this.board_collection_key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
var current_board = this.props.board;
|
||||
|
||||
this.isDragDisabled = false;
|
||||
|
||||
var lists = Collections.get('lists').findBy({ board_id: this.props.board.id });
|
||||
if (this.user_mode) {
|
||||
lists = [
|
||||
{
|
||||
id: 'allusertasks_' + this.props.board.id.split('_')[1],
|
||||
title: Languages.t('components.workspace.list_manager.all', [], 'All'),
|
||||
all: true,
|
||||
},
|
||||
];
|
||||
|
||||
var workspaces = [];
|
||||
TasksService.getTasksInList(
|
||||
this.props.board.id,
|
||||
lists[0].id,
|
||||
this.state.archived ? true : false,
|
||||
).forEach(task => {
|
||||
if (workspaces.indexOf(task.workspace_id) < 0) {
|
||||
workspaces.push(task.workspace_id);
|
||||
|
||||
var workspace = Collections.get('workspaces').find(task.workspace_id);
|
||||
|
||||
if (workspace) {
|
||||
lists.push({
|
||||
id:
|
||||
'workspaceusertasks_' + this.props.board.id.split('_')[1] + '_' + task.workspace_id,
|
||||
title: workspace.name,
|
||||
other_group: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lists.sort((a, b) => {
|
||||
if (a.all) {
|
||||
return -1;
|
||||
}
|
||||
if (a.other_group) {
|
||||
return 1;
|
||||
}
|
||||
if (b.other_group) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
this.isDragDisabled = true;
|
||||
}
|
||||
|
||||
var loading =
|
||||
(!this.user_mode &&
|
||||
!Collections.get('lists').did_load_first_time[this.board_collection_key]) ||
|
||||
!Collections.get('tasks').did_load_first_time[this.board_collection_key];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading">
|
||||
<Loader color="#CCC" className="app_loader" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TasksService.setElementIndexPool(
|
||||
'lists_' + this.props.board.id,
|
||||
Collections.get('lists').findBy({ board_id: this.props.board.id }),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
var lists = lists.sort(
|
||||
(a, b) =>
|
||||
TasksService.getElementIndex(a, 'lists_' + a.board_id) -
|
||||
TasksService.getElementIndex(b, 'lists_' + b.board_id),
|
||||
);
|
||||
if (!this.user_mode) {
|
||||
lists.push({
|
||||
id: 'add_list',
|
||||
render: (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
paddingRight: '40px',
|
||||
position: 'relative',
|
||||
top: '-8px',
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
style={{ display: 'inline-block' }}
|
||||
menu={[
|
||||
{
|
||||
type: 'title',
|
||||
text: Languages.t('scenes.apps.tasks.list_modal.new_list', [], 'New list'),
|
||||
},
|
||||
{
|
||||
type: 'react-element',
|
||||
reactElement: level => {
|
||||
return (
|
||||
<ListEditor
|
||||
menuLevel={level}
|
||||
board={this.props.board}
|
||||
collectionKey={this.board_collection_key}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Rounded
|
||||
text={Languages.t('scenes.apps.tasks.list_modal.new_list', [], 'New list')}
|
||||
className="list_add"
|
||||
/>
|
||||
</Menu>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropContext onDragStart={this.onDragStart} onDragEnd={this.onDragEnd}>
|
||||
<div className="board_header">
|
||||
{!this.props.noTitle && (
|
||||
<div className="app_title">
|
||||
{!((this.props.tab || {}).configuration || {}).board_id && (
|
||||
<div
|
||||
className="app_back_btn"
|
||||
onClick={() => {
|
||||
TasksService.openBoard(null);
|
||||
}}
|
||||
>
|
||||
{Languages.t('scenes.apps.board.all_boards', [], 'All Boards')}
|
||||
</div>
|
||||
)}
|
||||
{current_board.emoji && (
|
||||
<Emojione type={current_board.emoji} s32 className="board_emoji" />
|
||||
)}
|
||||
{current_board.user_image && (
|
||||
<User user={{ thumbnail: current_board.user_image }} medium />
|
||||
)}
|
||||
{current_board.title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="nomobile info" />
|
||||
|
||||
{!this.props.hideMore && (
|
||||
<div
|
||||
className="nomobile options app_right_btn"
|
||||
onClick={evt => {
|
||||
MenusManager.openMenu(
|
||||
[
|
||||
{
|
||||
type: 'title',
|
||||
text: Languages.t('scenes.apps.board.display_as', [], 'Afficher en tant que'),
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
icon: current_board.view_mode === 'grid' ? 'check' : ' ',
|
||||
className: current_board.view_mode === 'grid' ? 'primary' : ' ',
|
||||
rightIcon: 'window-restore',
|
||||
text: Languages.t('scenes.apps.board.kanban', [], 'Kanban'),
|
||||
onClick: () => {
|
||||
current_board.view_mode = 'grid';
|
||||
Collections.get('boards').save(
|
||||
current_board,
|
||||
this.props.boardsCollectionKey,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
icon: current_board.view_mode === 'list' ? 'check' : ' ',
|
||||
className: current_board.view_mode === 'list' ? 'primary' : ' ',
|
||||
rightIcon: 'list-ul',
|
||||
text: Languages.t('scenes.apps.calendar.calendar.list_btn', [], 'Liste'),
|
||||
onClick: () => {
|
||||
current_board.view_mode = 'list';
|
||||
Collections.get('boards').save(
|
||||
current_board,
|
||||
this.props.boardsCollectionKey,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'menu',
|
||||
icon: this.state.archived === false ? 'check' : ' ',
|
||||
className: this.state.archived === false ? 'primary' : ' ',
|
||||
text: Languages.t('scenes.apps.board.active_tasks', [], 'Tâches actives'),
|
||||
onClick: () => {
|
||||
this.setState({ archived: false });
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
icon: this.state.archived ? 'check' : ' ',
|
||||
className: this.state.archived ? 'primary' : ' ',
|
||||
text: Languages.t(
|
||||
'scenes.apps.board.archived_tasks',
|
||||
[
|
||||
Collections.get('tasks').findBy({
|
||||
archived: true,
|
||||
board_id: current_board.id,
|
||||
}).length,
|
||||
],
|
||||
'Tâches archivées ($1)',
|
||||
),
|
||||
onClick: () => {
|
||||
this.setState({ archived: true });
|
||||
},
|
||||
},
|
||||
],
|
||||
{ x: evt.clientX, y: evt.clientY },
|
||||
'bottom',
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Languages.t('general.more', [], 'Plus')}
|
||||
<ChevronDownIcon className="m-icon-small" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
'board ' +
|
||||
(this.props.inline ? 'inline ' : '') +
|
||||
(this.props.mode === 'list' ? 'mode_list ' : 'mode_grid ')
|
||||
}
|
||||
>
|
||||
<div className="lists_before">
|
||||
{this.props.mode === 'list' && (
|
||||
<Tabs
|
||||
tabs={lists
|
||||
.map(item => {
|
||||
return {
|
||||
id: item.front_id || item.id,
|
||||
titleClassName: item.id === 'add_list' ? 'no-selection-border' : '',
|
||||
titleStyle: { borderBottomColor: item.color },
|
||||
title: () => {
|
||||
if (item.id === 'add_list') {
|
||||
return item.render;
|
||||
}
|
||||
return (
|
||||
(item.title || '-') +
|
||||
' (' +
|
||||
TasksService.getTasksInList(
|
||||
item.board_id,
|
||||
item.id,
|
||||
this.state.archived ? true : false,
|
||||
).length +
|
||||
')'
|
||||
);
|
||||
},
|
||||
render: () => {
|
||||
if (item.id === 'add_list') {
|
||||
return '';
|
||||
}
|
||||
return (
|
||||
<List
|
||||
showArchived={this.state.archived}
|
||||
isDragDisabled={this.isDragDisabled}
|
||||
canCreate={!this.user_mode}
|
||||
list={item}
|
||||
board={this.props.board}
|
||||
collectionKey={this.board_collection_key}
|
||||
key={item.front_id}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
})
|
||||
.reduce((acc, cur) => {
|
||||
acc[cur.id] = cur;
|
||||
return acc;
|
||||
}, {})}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.props.mode === 'grid' && (
|
||||
<PerfectScrollbar className="lists_scrollable" options={{ suppressScrollX: false }}>
|
||||
<Droppable
|
||||
droppableId={'lists'}
|
||||
direction="horizontal"
|
||||
type="board"
|
||||
className="droppable_list"
|
||||
>
|
||||
{provided => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
style={{
|
||||
display: 'flex',
|
||||
minWidth: 100 + 280 * (lists.length - 1),
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{lists
|
||||
.filter(a => a.id !== 'add_list')
|
||||
.map(item => {
|
||||
return (
|
||||
<List
|
||||
showArchived={this.state.archived}
|
||||
isDragDisabled={this.isDragDisabled}
|
||||
canCreate={!this.user_mode}
|
||||
list={item}
|
||||
board={this.props.board}
|
||||
collectionKey={this.board_collection_key}
|
||||
key={item.front_id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{provided.placeholder}
|
||||
|
||||
{lists[lists.length - 1].render}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</PerfectScrollbar>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*<MainPlus onClick={()=>{
|
||||
MediumPopupManager.open(<TaskEditor collectionKey={this.board_collection_key} />, {size: {width: 600}});
|
||||
}} />*/}
|
||||
</DragDropContext>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
.board {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
|
||||
margin: 0 -32px;
|
||||
margin-bottom: -15px;
|
||||
margin-top: -8px;
|
||||
|
||||
&.inline {
|
||||
height: auto;
|
||||
.list,
|
||||
.component_tabs,
|
||||
.component_tabs > .body,
|
||||
.lists_before {
|
||||
display: block !important;
|
||||
height: auto !important;
|
||||
position: relative !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.list .list_draggable .droppable {
|
||||
max-height: none;
|
||||
height: auto;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.mode_list {
|
||||
.lists_before {
|
||||
padding-top: 16px;
|
||||
}
|
||||
.lists_before .component_tabs .body .scrollable_task_list {
|
||||
padding: 0px 24px;
|
||||
}
|
||||
.lists_before .component_tabs .component_tabs_tabs,
|
||||
.lists_before .component_tabs .body .add_task {
|
||||
height: auto;
|
||||
margin-right: 24px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
.component_tabs {
|
||||
height: 100%;
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.component_tabs > .body {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
& > .component_tabs_tab {
|
||||
&.no-selection-border {
|
||||
border-bottom: 0px;
|
||||
margin-left: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list .list_draggable .list_header {
|
||||
display: none;
|
||||
}
|
||||
.list {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.list,
|
||||
.list_draggable {
|
||||
margin-right: 0px;
|
||||
width: 100%;
|
||||
.task_draggable_parent {
|
||||
margin-bottom: 0px;
|
||||
.task {
|
||||
border-left: 0px;
|
||||
border-right: 0px;
|
||||
border-top: 0px;
|
||||
border-radius: 0px;
|
||||
display: flex;
|
||||
padding-right: 56px;
|
||||
|
||||
.task_options {
|
||||
padding-top: 2px;
|
||||
display: block;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.task_line_1 {
|
||||
padding-top: 2px;
|
||||
flex: 1;
|
||||
.task_title {
|
||||
line-height: 22px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tags-in-name {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
.task_line_2,
|
||||
.task_line_tags,
|
||||
.task_users {
|
||||
margin-top: 0px;
|
||||
width: 20%;
|
||||
max-width: 110px;
|
||||
}
|
||||
.task_line_tags {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
.attachmentPicker {
|
||||
width: auto;
|
||||
margin: 0px;
|
||||
.attachments .attachment {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-top: -8px;
|
||||
margin-bottom: -8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.attachments,
|
||||
.attachments .attachment > .file,
|
||||
.attachments .attachment > .file .preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
}
|
||||
.attachments .attachment > .file .data {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lists_before {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.lists_scrollable {
|
||||
white-space: nowrap;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
& > div:first-child {
|
||||
padding-left: 32px;
|
||||
padding-bottom: 16px;
|
||||
box-sizing: border-box;
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list_add {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
width: auto;
|
||||
padding: 4px;
|
||||
height: 32px;
|
||||
padding-right: 4px;
|
||||
padding-left: 8px;
|
||||
line-height: 24px;
|
||||
vertical-align: top;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.m-icon-small {
|
||||
vertical-align: middle;
|
||||
margin-top: -2px;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board_header {
|
||||
display: flex;
|
||||
|
||||
.app_title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.options {
|
||||
font-size: 13px;
|
||||
margin-left: 10px;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view_title {
|
||||
font-size: 24px;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
line-height: 24px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.filter_input {
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
margin-top: -2px;
|
||||
margin-right: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
margin-right: 5px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.board_parameters {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/no-direct-mutation-state */
|
||||
import React, { Component } from 'react';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import Collections from 'app/deprecated/CollectionsV1/Collections/Collections.js';
|
||||
import MenuManager from 'app/components/menus/menus-manager.jsx';
|
||||
import Workspaces from 'app/deprecated/workspaces/workspaces.jsx';
|
||||
|
||||
import InputWithIcon from 'components/inputs/input-with-icon';
|
||||
import Button from 'components/buttons/button.jsx';
|
||||
|
||||
export default class BoardEditor extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
i18n: Languages,
|
||||
name: '',
|
||||
emoji: '',
|
||||
};
|
||||
Languages.addListener(this);
|
||||
}
|
||||
UNSAFE_componentWillMount() {
|
||||
if (this.props.id) {
|
||||
var board = Collections.get('boards').find(this.props.id);
|
||||
if (board) {
|
||||
this.state.name = board.title;
|
||||
this.state.emoji = board.emoji;
|
||||
}
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
Languages.removeListener(this);
|
||||
}
|
||||
save() {
|
||||
var board = {};
|
||||
if (this.props.id) {
|
||||
board = Collections.get('boards').find(this.props.id);
|
||||
} else {
|
||||
board = Collections.get('boards').editCopy({});
|
||||
board.workspace_id = Workspaces.currentWorkspaceId;
|
||||
}
|
||||
board.title = this.state.name;
|
||||
board.emoji = this.state.emoji;
|
||||
Collections.get('boards').save(board, this.props.collectionKey);
|
||||
MenuManager.closeMenu();
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="menu-buttons bottom-margin">
|
||||
<InputWithIcon
|
||||
className="full_width"
|
||||
focusOnDidMount
|
||||
menu_level={this.props.menuLevel}
|
||||
placeholder={Languages.t('scenes.apps.tasks.board.place_holder', [], 'Board name')}
|
||||
value={[this.state.emoji, this.state.name]}
|
||||
onEnter={() => this.save()}
|
||||
onChange={value => {
|
||||
this.setState({ emoji: value[0], name: value[1] });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="menu-buttons">
|
||||
<Button
|
||||
disabled={(this.state.name || '').length <= 0}
|
||||
type="button"
|
||||
value={Languages.t('general.save', [], 'Enregistrer')}
|
||||
onClick={() => {
|
||||
this.save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import Input from 'components/inputs/input.jsx';
|
||||
import Button from 'components/buttons/button.jsx';
|
||||
import OutsideClickHandler from 'react-outside-click-handler';
|
||||
|
||||
export default class AddTask extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
i18n: Languages,
|
||||
unselected: true,
|
||||
};
|
||||
Languages.addListener(this);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
Languages.removeListener(this);
|
||||
}
|
||||
newTask() {
|
||||
this.setState({ new_task_title: '' });
|
||||
if (!(this.state.new_task_title || '').trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var task = {
|
||||
title: this.state.new_task_title,
|
||||
};
|
||||
|
||||
this.props.onSubmit(task);
|
||||
}
|
||||
render() {
|
||||
if (this.state.unselected) {
|
||||
return (
|
||||
<div
|
||||
className="add_task unselected"
|
||||
onClick={() => {
|
||||
this.setState({ unselected: false });
|
||||
}}
|
||||
>
|
||||
{Languages.t('scenes.apps.board.new_task', [], '+ New task')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OutsideClickHandler
|
||||
onOutsideClick={() => {
|
||||
this.setState({ unselected: true });
|
||||
}}
|
||||
>
|
||||
<div className="add_task">
|
||||
<Input
|
||||
autoFocus
|
||||
className="medium"
|
||||
placeholder={Languages.t('general.add', [], 'Add')}
|
||||
value={this.state.new_task_title}
|
||||
onChange={evt => {
|
||||
this.setState({ new_task_title: evt.target.value });
|
||||
}}
|
||||
onEnter={() => {
|
||||
this.newTask();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="small"
|
||||
value={Languages.t('general.add', [], 'Add')}
|
||||
onClick={() => {
|
||||
this.newTask();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</OutsideClickHandler>
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user