Compare commits

..
13 Commits
23 changed files with 273 additions and 34 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ COPY server .
RUN cp .env.sample .env RUN cp .env.sample .env
COPY --from=client-builder /app/build public COPY --from=client-builder /app/build public
COPY --from=client-builder /app/build/index.html views COPY --from=client-builder /app/build/index.html views/index.ejs
VOLUME /app/public/user-avatars VOLUME /app/public/user-avatars
VOLUME /app/public/project-background-images VOLUME /app/public/project-background-images
+56
View File
@@ -0,0 +1,56 @@
const fs = require('fs');
const path = require('path');
const BASE_URL_PLACEHOLDER = 'BASE_URL_PLACEHOLDER';
const replaceInFile = (file, search, replace) => {
fs.readFile(file, 'utf8', (readError, data) => {
if (readError) {
throw new Error(`${readError}`);
}
const res = data.replaceAll(search, replace);
fs.writeFile(file, res, 'utf8', (writeError) => {
if (writeError) {
throw new Error(`${writeError}`);
}
});
});
};
const replaceBaseUrl = (compiler) => {
compiler.hooks.assetEmitted.tap('ReplaceBaseUrlPlaceholder', (file, info) => {
if (info.content.indexOf(BASE_URL_PLACEHOLDER) >= 0) {
if (/\.css$/.exec(info.targetPath)) {
// For CSS 'url(...)' import we can use relative import
const relPath = path.relative(path.dirname(info.targetPath), info.outputPath);
replaceInFile(info.targetPath, BASE_URL_PLACEHOLDER, `${relPath}/`);
} else if (/\.js$/.exec(info.targetPath)) {
// For JS 'import ... from "some-asset"' we can get the variable injected in the window object
// eslint-disable-next-line no-template-curly-in-string
replaceInFile(info.targetPath, `"${BASE_URL_PLACEHOLDER}"`, '`${window.BASE_URL}/`');
} else if (/index\.html$/.exec(info.targetPath)) {
// For the main html file, we set a placeholder for sails to inject the correct value as runtime
replaceInFile(info.targetPath, BASE_URL_PLACEHOLDER, '<%= BASE_URL %>');
}
}
});
};
module.exports = function override(config, env) {
if (env === 'production') {
const plugins = config.plugins.map((plugin) => {
if (plugin.constructor.name === 'InterpolateHtmlPlugin') {
const newPlugin = plugin;
newPlugin.replacements.PUBLIC_URL = BASE_URL_PLACEHOLDER;
return newPlugin;
}
return plugin;
});
return {
...config,
output: { ...config.output, publicPath: BASE_URL_PLACEHOLDER },
plugins: [...plugins, { apply: replaceBaseUrl }],
};
}
return config;
};
+53
View File
@@ -6,6 +6,7 @@
"": { "": {
"name": "planka-client", "name": "planka-client",
"dependencies": { "dependencies": {
"@juggle/resize-observer": "^3.4.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"connected-react-router": "^6.9.3", "connected-react-router": "^6.9.3",
"date-fns": "^2.29.1", "date-fns": "^2.29.1",
@@ -62,6 +63,7 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"jest-enzyme": "^7.1.2", "jest-enzyme": "^7.1.2",
"prettier": "2.7.1", "prettier": "2.7.1",
"react-app-rewired": "^2.2.1",
"react-test-renderer": "^17.0.2" "react-test-renderer": "^17.0.2"
} }
}, },
@@ -3030,6 +3032,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@juggle/resize-observer": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
},
"node_modules/@leichtgewicht/ip-codec": { "node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
@@ -18659,6 +18666,30 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/react-app-rewired": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.2.1.tgz",
"integrity": "sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==",
"dev": true,
"dependencies": {
"semver": "^5.6.0"
},
"bin": {
"react-app-rewired": "bin/index.js"
},
"peerDependencies": {
"react-scripts": ">=2.1.3"
}
},
"node_modules/react-app-rewired/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/react-beautiful-dnd": { "node_modules/react-beautiful-dnd": {
"version": "13.1.0", "version": "13.1.0",
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
@@ -27333,6 +27364,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"@juggle/resize-observer": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
},
"@leichtgewicht/ip-codec": { "@leichtgewicht/ip-codec": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
@@ -38706,6 +38742,23 @@
"whatwg-fetch": "^3.6.2" "whatwg-fetch": "^3.6.2"
} }
}, },
"react-app-rewired": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-app-rewired/-/react-app-rewired-2.2.1.tgz",
"integrity": "sha512-uFQWTErXeLDrMzOJHKp0h8P1z0LV9HzPGsJ6adOtGlA/B9WfT6Shh4j2tLTTGlXOfiVx6w6iWpp7SOC5pvk+gA==",
"dev": true,
"requires": {
"semver": "^5.6.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
},
"react-beautiful-dnd": { "react-beautiful-dnd": {
"version": "13.1.0", "version": "13.1.0",
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
+6 -4
View File
@@ -2,11 +2,11 @@
"name": "planka-client", "name": "planka-client",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "react-scripts build", "build": "react-app-rewired build",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"lint": "eslint --ext js,jsx src", "lint": "eslint --ext js,jsx src config-overrides.js",
"start": "react-scripts start", "start": "react-app-rewired start",
"test": "react-scripts test" "test": "react-app-rewired test"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@@ -63,6 +63,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@juggle/resize-observer": "^3.4.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"connected-react-router": "^6.9.3", "connected-react-router": "^6.9.3",
"date-fns": "^2.29.1", "date-fns": "^2.29.1",
@@ -119,6 +120,7 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"jest-enzyme": "^7.1.2", "jest-enzyme": "^7.1.2",
"prettier": "2.7.1", "prettier": "2.7.1",
"react-app-rewired": "^2.2.1",
"react-test-renderer": "^17.0.2" "react-test-renderer": "^17.0.2"
} }
} }
+1
View File
@@ -26,6 +26,7 @@
--> -->
<title>Planka</title> <title>Planka</title>
</head> </head>
<script>window.BASE_URL = "%PUBLIC_URL%";</script>
<body id="app"> <body id="app">
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
+2 -1
View File
@@ -5,7 +5,7 @@ import Config from '../constants/Config';
const io = sailsIOClient(socketIOClient); const io = sailsIOClient(socketIOClient);
io.sails.url = Config.SERVER_BASE_URL; io.sails.url = Config.SERVER_HOST_NAME;
io.sails.autoConnect = false; io.sails.autoConnect = false;
io.sails.reconnection = true; io.sails.reconnection = true;
io.sails.useCORSRouteToGetCookie = false; io.sails.useCORSRouteToGetCookie = false;
@@ -13,6 +13,7 @@ io.sails.environment = process.env.NODE_ENV;
const { socket } = io; const { socket } = io;
socket.path = `${Config.BASE_PATH}/socket.io`;
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => { ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -19,7 +19,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
...defaultData, ...defaultData,
})); }));
const handleRoleSelectClick = useCallback((role) => { const handleSelectRoleClick = useCallback((role) => {
setData((prevData) => ({ setData((prevData) => ({
...prevData, ...prevData,
role, role,
@@ -50,7 +50,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
<Menu secondary vertical className={styles.menu}> <Menu secondary vertical className={styles.menu}>
<Menu.Item <Menu.Item
active={data.role === BoardMembershipRoles.EDITOR} active={data.role === BoardMembershipRoles.EDITOR}
onClick={() => handleRoleSelectClick(BoardMembershipRoles.EDITOR)} onClick={() => handleSelectRoleClick(BoardMembershipRoles.EDITOR)}
> >
<div className={styles.menuItemTitle}>{t('common.editor')}</div> <div className={styles.menuItemTitle}>{t('common.editor')}</div>
<div className={styles.menuItemDescription}> <div className={styles.menuItemDescription}>
@@ -59,7 +59,7 @@ const BoardMembershipPermissionsSelectStep = React.memo(
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
active={data.role === BoardMembershipRoles.VIEWER} active={data.role === BoardMembershipRoles.VIEWER}
onClick={() => handleRoleSelectClick(BoardMembershipRoles.VIEWER)} onClick={() => handleSelectRoleClick(BoardMembershipRoles.VIEWER)}
> >
<div className={styles.menuItemTitle}>{t('common.viewer')}</div> <div className={styles.menuItemTitle}>{t('common.viewer')}</div>
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div> <div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
+19 -4
View File
@@ -5,6 +5,7 @@ import { Button, Icon } from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { startTimer, stopTimer } from '../../utils/timer';
import Paths from '../../constants/Paths'; import Paths from '../../constants/Paths';
import Tasks from './Tasks'; import Tasks from './Tasks';
import NameEdit from './NameEdit'; import NameEdit from './NameEdit';
@@ -57,6 +58,17 @@ const Card = React.memo(
} }
}, []); }, []);
const handleToggleTimerClick = useCallback(
(event) => {
event.preventDefault();
onUpdate({
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
});
},
[timer, onUpdate],
);
const handleNameUpdate = useCallback( const handleNameUpdate = useCallback(
(newName) => { (newName) => {
onUpdate({ onUpdate({
@@ -108,7 +120,13 @@ const Card = React.memo(
)} )}
{timer && ( {timer && (
<span className={classNames(styles.attachment, styles.attachmentLeft)}> <span className={classNames(styles.attachment, styles.attachmentLeft)}>
<Timer startedAt={timer.startedAt} total={timer.total} size="tiny" /> <Timer
as="span"
startedAt={timer.startedAt}
total={timer.total}
size="tiny"
onClick={canEdit ? handleToggleTimerClick : undefined}
/>
</span> </span>
)} )}
</span> </span>
@@ -148,14 +166,11 @@ const Card = React.memo(
{canEdit && ( {canEdit && (
<ActionsPopup <ActionsPopup
card={{ card={{
id,
name,
dueDate, dueDate,
timer, timer,
boardId, boardId,
listId, listId,
projectId, projectId,
isPersisted,
}} }}
projectsToLists={allProjectsToLists} projectsToLists={allProjectsToLists}
boardMemberships={allBoardMemberships} boardMemberships={allBoardMemberships}
@@ -78,7 +78,9 @@
.cover { .cover {
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
margin-bottom: 2px; margin-bottom: 2px;
padding: 2px 2px 0;
vertical-align: middle; vertical-align: middle;
width: 100%;
} }
.details { .details {
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { Button, Grid, Icon, Modal } from 'semantic-ui-react'; import { Button, Grid, Icon, Modal } from 'semantic-ui-react';
import { Markdown } from '../../lib/custom-ui'; import { Markdown } from '../../lib/custom-ui';
import { startTimer, stopTimer } from '../../utils/timer';
import NameField from './NameField'; import NameField from './NameField';
import DescriptionEdit from './DescriptionEdit'; import DescriptionEdit from './DescriptionEdit';
import Tasks from './Tasks'; import Tasks from './Tasks';
@@ -80,6 +81,12 @@ const CardModal = React.memo(
const isGalleryOpened = useRef(false); const isGalleryOpened = useRef(false);
const handleToggleTimerClick = useCallback(() => {
onUpdate({
timer: timer.startedAt ? stopTimer(timer) : startTimer(timer),
});
}, [timer, onUpdate]);
const handleNameUpdate = useCallback( const handleNameUpdate = useCallback(
(newName) => { (newName) => {
onUpdate({ onUpdate({
@@ -291,6 +298,19 @@ const CardModal = React.memo(
<Timer startedAt={timer.startedAt} total={timer.total} /> <Timer startedAt={timer.startedAt} total={timer.total} />
)} )}
</span> </span>
{canEdit && (
<button
onClick={handleToggleTimerClick}
type="button"
className={classNames(styles.attachment, styles.dueDate)}
>
<Icon
name={timer.startedAt ? 'pause' : 'play'}
size="small"
className={styles.addAttachment}
/>
</button>
)}
</div> </div>
)} )}
</div> </div>
+8 -3
View File
@@ -14,7 +14,7 @@ const SIZES = {
MEDIUM: 'medium', MEDIUM: 'medium',
}; };
const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => { const Timer = React.memo(({ as, startedAt, total, size, isDisabled, onClick }) => {
const prevStartedAt = usePrevious(startedAt); const prevStartedAt = usePrevious(startedAt);
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
@@ -52,6 +52,7 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
className={classNames( className={classNames(
styles.wrapper, styles.wrapper,
styles[`wrapper${upperFirst(size)}`], styles[`wrapper${upperFirst(size)}`],
startedAt && styles.wrapperActive,
onClick && styles.wrapperHoverable, onClick && styles.wrapperHoverable,
)} )}
> >
@@ -59,16 +60,19 @@ const Timer = React.memo(({ startedAt, total, size, isDisabled, onClick }) => {
</span> </span>
); );
const ElementType = as;
return onClick ? ( return onClick ? (
<button type="button" disabled={isDisabled} className={styles.button} onClick={onClick}> <ElementType type="button" disabled={isDisabled} className={styles.button} onClick={onClick}>
{contentNode} {contentNode}
</button> </ElementType>
) : ( ) : (
contentNode contentNode
); );
}); });
Timer.propTypes = { Timer.propTypes = {
as: PropTypes.elementType,
startedAt: PropTypes.instanceOf(Date), startedAt: PropTypes.instanceOf(Date),
total: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types total: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
size: PropTypes.oneOf(Object.values(SIZES)), size: PropTypes.oneOf(Object.values(SIZES)),
@@ -77,6 +81,7 @@ Timer.propTypes = {
}; };
Timer.defaultProps = { Timer.defaultProps = {
as: 'button',
startedAt: undefined, startedAt: undefined,
size: SIZES.MEDIUM, size: SIZES.MEDIUM,
isDisabled: false, isDisabled: false,
+13 -3
View File
@@ -9,17 +9,28 @@
} }
.wrapper { .wrapper {
background: #f3f4f6; background: #dce0e4;
border: none; border: none;
border-radius: 3px; border-radius: 3px;
color: #7e8ea3; color: #6a808b;
display: inline-block; display: inline-block;
font-variant-numeric: tabular-nums;
outline: none; outline: none;
text-align: left; text-align: left;
transition: background 0.3s ease; transition: background 0.3s ease;
vertical-align: top; vertical-align: top;
} }
.wrapperActive {
background: #21ba45;
color: #fff;
&.wrapperHoverable:hover {
background: #16ab39;
color: #fff;
}
}
.wrapperHoverable:hover { .wrapperHoverable:hover {
background: #d2d8dc; background: #d2d8dc;
color: #17394d; color: #17394d;
@@ -42,6 +53,5 @@
.wrapperMedium { .wrapperMedium {
line-height: 20px; line-height: 20px;
padding: 6px 12px; padding: 6px 12px;
text-decoration: underline;
} }
} }
@@ -0,0 +1,19 @@
import React from 'react';
// import { useTranslation } from 'react-i18next';
import { Image, Tab } from 'semantic-ui-react';
import logo from '../../assets/images/logo.png';
import styles from './AboutPane.module.scss';
const AboutPane = React.memo(() => {
// const [t] = useTranslation();
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<Image centered src={logo} size="large" className={styles.logo} />
</Tab.Pane>
);
});
export default AboutPane;
@@ -0,0 +1,10 @@
:global(#app) {
/* .logo {
margin-bottom: 16px;
} */
.wrapper {
border: none;
box-shadow: none;
}
}
@@ -5,6 +5,7 @@ import { Modal, Tab } from 'semantic-ui-react';
import AccountPane from './AccountPane'; import AccountPane from './AccountPane';
import PreferencesPane from './PreferencesPane'; import PreferencesPane from './PreferencesPane';
import AboutPane from './AboutPane';
const UserSettingsModal = React.memo( const UserSettingsModal = React.memo(
({ ({
@@ -71,6 +72,10 @@ const UserSettingsModal = React.memo(
<PreferencesPane subscribeToOwnCards={subscribeToOwnCards} onUpdate={onUpdate} /> <PreferencesPane subscribeToOwnCards={subscribeToOwnCards} onUpdate={onUpdate} />
), ),
}, },
{
menuItem: 'About Planka',
render: () => <AboutPane />,
},
]; ];
return ( return (
+8 -1
View File
@@ -1,6 +1,11 @@
const { BASE_URL } = window;
const BASE_PATH = BASE_URL.replace(/^.*\/\/[^/]*(.*)[^?#]*.*$/, '$1');
const SERVER_BASE_URL = const SERVER_BASE_URL =
process.env.REACT_APP_SERVER_BASE_URL || process.env.REACT_APP_SERVER_BASE_URL ||
(process.env.NODE_ENV === 'production' ? '' : 'http://localhost:1337'); (process.env.NODE_ENV === 'production' ? BASE_URL : 'http://localhost:1337');
const SERVER_HOST_NAME = SERVER_BASE_URL.replace(/^(.*\/\/[^/?#]*).*$/, '$1');
const ACCESS_TOKEN_KEY = 'accessToken'; const ACCESS_TOKEN_KEY = 'accessToken';
const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion'; const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion';
@@ -10,7 +15,9 @@ const POSITION_GAP = 65535;
const ACTIVITIES_LIMIT = 50; const ACTIVITIES_LIMIT = 50;
export default { export default {
BASE_PATH,
SERVER_BASE_URL, SERVER_BASE_URL,
SERVER_HOST_NAME,
ACCESS_TOKEN_KEY, ACCESS_TOKEN_KEY,
ACCESS_TOKEN_VERSION_KEY, ACCESS_TOKEN_VERSION_KEY,
ACCESS_TOKEN_VERSION, ACCESS_TOKEN_VERSION,
+7 -5
View File
@@ -1,8 +1,10 @@
const ROOT = '/'; import Config from './Config';
const LOGIN = '/login';
const PROJECTS = '/projects/:id'; const ROOT = `${Config.BASE_PATH}/`;
const BOARDS = '/boards/:id'; const LOGIN = `${Config.BASE_PATH}/login`;
const CARDS = '/cards/:id'; const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
const BOARDS = `${Config.BASE_PATH}/boards/:id`;
const CARDS = `${Config.BASE_PATH}/cards/:id`;
export default { export default {
ROOT, ROOT,
+34 -4
View File
@@ -1,4 +1,5 @@
import React, { useCallback, useState } from 'react'; import { ResizeObserver } from '@juggle/resize-observer';
import React, { useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react'; import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
@@ -8,6 +9,9 @@ export default (WrappedComponent, defaultProps) => {
const Popup = React.memo(({ children, ...props }) => { const Popup = React.memo(({ children, ...props }) => {
const [isOpened, setIsOpened] = useState(false); const [isOpened, setIsOpened] = useState(false);
const wrapper = useRef(null);
const resizeObserver = useRef(null);
const handleOpen = useCallback(() => { const handleOpen = useCallback(() => {
setIsOpened(true); setIsOpened(true);
}, []); }, []);
@@ -37,6 +41,29 @@ export default (WrappedComponent, defaultProps) => {
[children], [children],
); );
const handleContentRef = useCallback((element) => {
if (resizeObserver.current) {
resizeObserver.current.disconnect();
}
if (!element) {
resizeObserver.current = null;
return;
}
resizeObserver.current = new ResizeObserver(() => {
if (resizeObserver.current.isInitial) {
resizeObserver.current.isInitial = false;
return;
}
wrapper.current.positionUpdate();
});
resizeObserver.current.isInitial = true;
resizeObserver.current.observe(element);
}, []);
const tigger = React.cloneElement(children, { const tigger = React.cloneElement(children, {
onClick: handleTriggerClick, onClick: handleTriggerClick,
}); });
@@ -45,6 +72,7 @@ export default (WrappedComponent, defaultProps) => {
<SemanticUIPopup <SemanticUIPopup
basic basic
wide wide
ref={wrapper}
trigger={tigger} trigger={tigger}
on="click" on="click"
open={isOpened} open={isOpened}
@@ -64,9 +92,11 @@ export default (WrappedComponent, defaultProps) => {
onClick={handleClick} onClick={handleClick}
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading {...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
> >
<Button icon="close" onClick={handleClose} className={styles.closeButton} /> <div ref={handleContentRef}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */} <Button icon="close" onClick={handleClose} className={styles.closeButton} />
<WrappedComponent {...props} onClose={handleClose} /> {/* eslint-disable-next-line react/jsx-props-no-spreading */}
<WrappedComponent {...props} onClose={handleClose} />
</div>
</SemanticUIPopup> </SemanticUIPopup>
); );
}); });
+1 -1
View File
@@ -5,7 +5,7 @@ import core from './core';
export default { export default {
language: 'en', language: 'en',
country: 'us', country: 'gb',
name: 'English', name: 'English',
embeddedLocale: merge(login, core), embeddedLocale: merge(login, core),
}; };
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "planka", "name": "planka",
"version": "1.8.0", "version": "1.8.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "planka", "name": "planka",
"version": "1.8.0", "version": "1.8.3",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "planka", "name": "planka",
"version": "1.8.0", "version": "1.8.3",
"private": true, "private": true,
"homepage": "https://plankanban.github.io/planka", "homepage": "https://plankanban.github.io/planka",
"repository": { "repository": {
+2 -1
View File
@@ -24,7 +24,7 @@ module.exports.views = {
* *
*/ */
extension: 'html', extension: 'ejs',
/** /**
* *
@@ -36,4 +36,5 @@ module.exports.views = {
*/ */
layout: false, layout: false,
locals: { BASE_URL: process.env.BASE_URL },
}; };