Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94dc83706c | ||
|
|
b08e731419 | ||
|
|
1ce0fac281 | ||
|
|
e3d00c8791 | ||
|
|
e1a0b635ea | ||
|
|
cc7959bb15 | ||
|
|
56044e6d8a | ||
|
|
e5bd73f33e | ||
|
|
e65ab6ece7 | ||
|
|
14cc55a8c0 | ||
|
|
b4306d0a4b | ||
|
|
283c850ebf | ||
|
|
62de71bd97 |
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
Generated
+53
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 |
+3
-3
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+2
-2
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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 },
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user