diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index 97795212a4..a010fde68f 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -53,12 +53,21 @@ const styles = (theme: { workspaces: { drawer: { width: any } } }) => ({ // width: `calc(100% + ${theme.workspaces.drawer.width}px)`, width: '100%', transition, + }, + appContentTransformTextWorkspace: { transform() { return workspaceStore.isWorkspaceDrawerOpen ? 'translateX(0)' : `translateX(-${theme.workspaces.drawer.width}px)`; }, }, + appContentTransformIconWorkspace: { + transform() { + return workspaceStore.isWorkspaceDrawerOpen + ? 'translateX(0)' + : 'translateX(-75px)'; + }, + }, titleBar: { display: 'block', zIndex: 1, @@ -149,7 +158,13 @@ class AppLayout extends Component, IState> { className={classes.titleBar} /> )} -
+
{workspacesDrawer} {sidebar}
diff --git a/src/components/settings/settings/EditSettingsForm.tsx b/src/components/settings/settings/EditSettingsForm.tsx index 6a79fcc8fc..cb825a7d79 100644 --- a/src/components/settings/settings/EditSettingsForm.tsx +++ b/src/components/settings/settings/EditSettingsForm.tsx @@ -821,6 +821,8 @@ class EditSettingsForm extends Component { + +
)} diff --git a/src/components/ui/WorkspaceIcon.tsx b/src/components/ui/WorkspaceIcon.tsx new file mode 100644 index 0000000000..7ceeeb97c6 --- /dev/null +++ b/src/components/ui/WorkspaceIcon.tsx @@ -0,0 +1,71 @@ +import { Component, ReactNode } from 'react'; +import { observer } from 'mobx-react'; +import withStyles, { WithStylesProps } from 'react-jss'; +import classnames from 'classnames'; +import { mdiInfinity } from '@mdi/js'; +import Icon from './icon'; + +const styles = theme => ({ + root: { + height: `${theme.serviceIcon.width}px`, + width: `${theme.serviceIcon.width}px`, + textAlign: 'center', + display: 'table-cell', + verticalAlign: 'middle', + }, + icon: { + height: 'auto', + width: `${theme.serviceIcon.width}px`, + maxHeight: `${theme.serviceIcon.width}px`, + }, + iconLetterContainer: { + border: `1px solid ${theme.colorText}`, + borderRadius: '20%', + }, + iconLetter: { + fontSize: 'x-large', + fontWeight: '500', + }, +}); + +interface IProps extends WithStylesProps { + className?: string; + name: string | null; + iconUrl: string; +} + +@observer +class WorkspaceIcon extends Component { + render(): ReactNode { + const { classes, className, name, iconUrl } = this.props; + if (iconUrl === 'allServices') { + return ( +
+ +
+ ); + } + if (iconUrl) { + return ( +
+ {name{' '} +
+ ); + } + return ( +
+ + {(name || ' ').slice(0, 1).toLocaleUpperCase()} + +
+ ); + } +} + +export default withStyles(styles, { injectTheme: true })(WorkspaceIcon); diff --git a/src/config.ts b/src/config.ts index 055fc9ad12..d669ea1503 100644 --- a/src/config.ts +++ b/src/config.ts @@ -420,6 +420,7 @@ export const DEFAULT_APP_SETTINGS = { hideSettingsButton: false, hideDownloadButton: false, alwaysShowWorkspaces: false, + useWorkspaceDrawerIconStyle: true, liftSingleInstanceLock: false, enableLongPressServiceHint: false, isTodosFeatureEnabled: true, diff --git a/src/containers/layout/AppLayoutContainer.tsx b/src/containers/layout/AppLayoutContainer.tsx index 8748f10322..f31f1de1ef 100644 --- a/src/containers/layout/AppLayoutContainer.tsx +++ b/src/containers/layout/AppLayoutContainer.tsx @@ -80,6 +80,7 @@ class AppLayoutContainer extends Component { ? workspaceStore.getWorkspaceServices(workspace).map(s => s.name) : services.all.map(s => s.name) } + useIconDisplayStyle={settings.all.app.useWorkspaceDrawerIconStyle} /> ); diff --git a/src/containers/settings/EditSettingsScreen.tsx b/src/containers/settings/EditSettingsScreen.tsx index 272297700b..d2d9610776 100644 --- a/src/containers/settings/EditSettingsScreen.tsx +++ b/src/containers/settings/EditSettingsScreen.tsx @@ -263,6 +263,10 @@ const messages = defineMessages({ id: 'settings.app.form.alwaysShowWorkspaces', defaultMessage: 'Always show workspace drawer', }, + useWorkspaceDrawerIconStyle: { + id: 'settings.app.form.useWorkspaceDrawerIconStyle', + defaultMessage: 'Use workspace drawer icon style', + }, accentColor: { id: 'settings.app.form.accentColor', defaultMessage: 'Accent color', @@ -435,6 +439,9 @@ class EditSettingsScreen extends Component< hideSettingsButton: Boolean(settingsData.hideSettingsButton), hideDownloadButton: Boolean(settingsData.hideDownloadButton), alwaysShowWorkspaces: Boolean(settingsData.alwaysShowWorkspaces), + useWorkspaceDrawerIconStyle: Boolean( + settingsData.useWorkspaceDrawerIconStyle, + ), accentColor: settingsData.accentColor, progressbarAccentColor: settingsData.progressbarAccentColor, showMessageBadgeWhenMuted: Boolean( @@ -1120,6 +1127,15 @@ class EditSettingsScreen extends Component< default: DEFAULT_APP_SETTINGS.alwaysShowWorkspaces, type: 'checkbox', }, + useWorkspaceDrawerIconStyle: { + label: intl.formatMessage(messages.useWorkspaceDrawerIconStyle), + value: ifUndefined( + settings.all.app.useWorkspaceDrawerIconStyle, + DEFAULT_APP_SETTINGS.useWorkspaceDrawerIconStyle, + ), + default: DEFAULT_APP_SETTINGS.useWorkspaceDrawerIconStyle, + type: 'checkbox', + }, accentColor: { label: intl.formatMessage(messages.accentColor), value: ifUndefined( diff --git a/src/features/appearance/index.ts b/src/features/appearance/index.ts index 46403275ec..306b371329 100644 --- a/src/features/appearance/index.ts +++ b/src/features/appearance/index.ts @@ -330,7 +330,11 @@ function generateShowDragAreaStyle(accentColor) { `; } -function generateVerticalStyle(widthStr, alwaysShowWorkspaces) { +function generateVerticalStyle( + widthStr, + alwaysShowWorkspaces, + useWorkspaceDrawerIconStyle, +) { if (!document.querySelector('#vertical-style')) { const link = document.createElement('link'); link.id = 'vertical-style'; @@ -343,13 +347,14 @@ function generateVerticalStyle(widthStr, alwaysShowWorkspaces) { const width = Number(widthStr); const sidebarWidth = width - 4; const verticalStyleOffset = 29; + const drawerWidth = useWorkspaceDrawerIconStyle ? '75px' : '300px'; return ` .sidebar { ${ - alwaysShowWorkspaces + alwaysShowWorkspaces || useWorkspaceDrawerIconStyle ? ` - width: calc(100% - 300px) !important; + width: calc(100% - ${drawerWidth}) !important; ` : '' } @@ -395,6 +400,7 @@ function generateStyle(settings, app) { useHorizontalStyle, alwaysShowWorkspaces, showServiceName, + useWorkspaceDrawerIconStyle, } = settings; const { isFullScreen } = app; @@ -423,7 +429,11 @@ function generateStyle(settings, app) { style += generateShowDragAreaStyle(accentColor); } if (useHorizontalStyle) { - style += generateVerticalStyle(serviceRibbonWidth, alwaysShowWorkspaces); + style += generateVerticalStyle( + serviceRibbonWidth, + alwaysShowWorkspaces, + useWorkspaceDrawerIconStyle, + ); } else if (document.querySelector('#vertical-style')) { const link = document.querySelector('#vertical-style'); if (link) { diff --git a/src/features/workspaces/api.ts b/src/features/workspaces/api.ts index b8bb703cd5..fd9ca315e6 100644 --- a/src/features/workspaces/api.ts +++ b/src/features/workspaces/api.ts @@ -51,7 +51,7 @@ export const workspaceApi = { const url = `${apiBase()}/workspace/${workspace.id}`; const options = { method: 'PUT', - body: JSON.stringify(pick(workspace, ['name', 'services'])), + body: JSON.stringify(pick(workspace, ['name', 'services', 'iconUrl'])), }; debug('updateWorkspace UPDATE', url, options); const result = await sendAuthRequest(url, options); diff --git a/src/features/workspaces/components/EditWorkspaceForm.tsx b/src/features/workspaces/components/EditWorkspaceForm.tsx index d902d29be5..1f98d74f05 100644 --- a/src/features/workspaces/components/EditWorkspaceForm.tsx +++ b/src/features/workspaces/components/EditWorkspaceForm.tsx @@ -30,6 +30,10 @@ const messages = defineMessages({ id: 'settings.workspace.form.name', defaultMessage: 'Name', }, + iconUrl: { + id: 'settings.workspace.form.iconUrl', + defaultMessage: 'Workspace icon url', + }, yourWorkspaces: { id: 'settings.workspace.form.yourWorkspaces', defaultMessage: 'Your workspaces', @@ -107,6 +111,12 @@ class EditWorkspaceForm extends Component { value: workspace.name, validators: [required], }, + iconUrl: { + label: intl.formatMessage(messages.iconUrl), + placeholder: intl.formatMessage(messages.iconUrl), + value: workspace.iconUrl || '', + validators: [], + }, keepLoaded: { label: intl.formatMessage(messages.keepLoaded), value: workspace.services.includes(KEEP_WS_LOADED_USID), @@ -181,6 +191,7 @@ class EditWorkspaceForm extends Component { )}
+

{intl.formatMessage(messages.keepLoadedInfo)} diff --git a/src/features/workspaces/components/WorkspaceDrawer.tsx b/src/features/workspaces/components/WorkspaceDrawer.tsx index 3e05cb7de9..88404ab284 100644 --- a/src/features/workspaces/components/WorkspaceDrawer.tsx +++ b/src/features/workspaces/components/WorkspaceDrawer.tsx @@ -40,16 +40,25 @@ const messages = defineMessages({ const styles = theme => ({ drawer: { background: theme.workspaces.drawer.background, - width: `${theme.workspaces.drawer.width}px`, display: 'flex', flexDirection: 'column', }, + drawerWithText: { + width: `${theme.workspaces.drawer.width}px`, + }, + drawerWithIcon: { + width: '75px', + }, headline: { fontSize: '24px', marginTop: '38px', marginBottom: '25px', marginLeft: theme.workspaces.drawer.padding, }, + headlineWithicon: { + marginLeft: '0', + color: theme.workspaces.drawer.buttons.color, + }, workspacesSettingsButton: { float: 'right', marginRight: theme.workspaces.drawer.padding, @@ -90,6 +99,7 @@ const styles = theme => ({ interface IProps extends WithStylesProps, WrappedComponentProps { getServicesForWorkspace: (workspace: Workspace | null) => string[]; + useIconDisplayStyle: boolean; } @observer @@ -104,19 +114,24 @@ class WorkspaceDrawer extends Component { } render(): ReactElement { - const { classes, getServicesForWorkspace } = this.props; + const { classes, getServicesForWorkspace, useIconDisplayStyle } = + this.props; const { intl } = this.props; const { activeWorkspace, isSwitchingWorkspace, nextWorkspace, workspaces } = workspaceStore; const actualWorkspace = isSwitchingWorkspace ? nextWorkspace : activeWorkspace; + + const drawerWithClass = useIconDisplayStyle + ? classes.drawerWithIcon + : classes.drawerWithText; return ( -

-

- {intl.formatMessage(messages.headline)} - + {useIconDisplayStyle ? ( +

+ + ) : ( +

+ {intl.formatMessage(messages.headline)} + { + workspaceActions.openWorkspaceSettings(); + }} + data-tip={`${intl.formatMessage( + messages.workspacesSettingsTooltip, + )}`} + > + + +

+ )} +
{ workspaceActions.deactivate(); workspaceActions.toggleWorkspaceDrawer(); @@ -148,6 +186,8 @@ class WorkspaceDrawer extends Component { { if (actualWorkspace === workspace) { @@ -172,9 +212,12 @@ class WorkspaceDrawer extends Component { > - {intl.formatMessage(messages.addNewWorkspaceLabel)} + {useIconDisplayStyle !== true && ( + {intl.formatMessage(messages.addNewWorkspaceLabel)} + )}{' '}
({ marginTop: '4px', color: theme.workspaces.drawer.listItem.name.color, }, + icon: { + maxWidth: '30px', + maxHeight: '30px', + margin: 'auto', + }, activeName: { color: theme.workspaces.drawer.listItem.name.activeColor, }, @@ -70,6 +76,8 @@ const styles = theme => ({ interface IProps extends WithStylesProps, WrappedComponentProps { isActive: boolean; name: string; + iconUrl: string; + useIconDisplayStyle: boolean; onClick: MouseEventHandler; services: string[]; onContextMenuEditClick?: (() => void) | null; @@ -83,6 +91,8 @@ class WorkspaceDrawerItem extends Component { classes, isActive, name, + iconUrl, + useIconDisplayStyle, onClick, onContextMenuEditClick = null, services, @@ -125,24 +135,30 @@ class WorkspaceDrawerItem extends Component { `${cmdOrCtrlShortcutKey(false)}+${altKey(false)}`, )} > - - {name} - - - {services.length > 0 - ? services.join(', ') - : intl.formatMessage(messages.noServicesAddedYet)} - + {useIconDisplayStyle === true ? ( + + ) : ( + <> + + {name} + + + {services.length > 0 + ? services.join(', ') + : intl.formatMessage(messages.noServicesAddedYet)} + + + )}
); } diff --git a/src/features/workspaces/components/WorkspaceItem.tsx b/src/features/workspaces/components/WorkspaceItem.tsx index 36c6ed2f57..302e256e2f 100644 --- a/src/features/workspaces/components/WorkspaceItem.tsx +++ b/src/features/workspaces/components/WorkspaceItem.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react'; import withStyles, { WithStylesProps } from 'react-jss'; import { noop } from 'lodash'; import Workspace from '../models/Workspace'; +import WorkspaceIcon from '../../../components/ui/WorkspaceIcon'; const styles = theme => ({ row: { @@ -12,7 +13,10 @@ const styles = theme => ({ background: theme.workspaces.settings.listItems.hoverBgColor, }, }, - columnName: {}, + columnName: { + width: '60px', + borderCollapse: 'separate', + }, }); interface IProps extends WithStylesProps { @@ -27,6 +31,13 @@ class WorkspaceItem extends Component { return ( + onItemClick(workspace)} + onKeyDown={noop} + > + + onItemClick(workspace)} onKeyDown={noop}> {workspace.name} diff --git a/src/features/workspaces/models/Workspace.ts b/src/features/workspaces/models/Workspace.ts index 98086289f3..4df1feac37 100644 --- a/src/features/workspaces/models/Workspace.ts +++ b/src/features/workspaces/models/Workspace.ts @@ -7,6 +7,8 @@ export default class Workspace { @observable name = null; + @observable iconUrl = ''; + @observable order = null; @observable services: string[] = []; @@ -22,6 +24,7 @@ export default class Workspace { this.id = data.id; this.name = data.name; + this.iconUrl = data.iconUrl; this.order = data.order; let { services } = data; diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 2781b67b75..88504dac5f 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -265,6 +265,7 @@ "settings.app.form.useHorizontalStyle": "Use horizontal style", "settings.app.form.useSelfSignedCertificates": "Enable self-signed certificates", "settings.app.form.useTouchIdToUnlock": "Allow using TouchID to unlock Ferdium", + "settings.app.form.useWorkspaceDrawerIconStyle": "Use workspace drawer icon style", "settings.app.form.wakeUpHibernationSplay": "Splay hibernate/wake cycles to reduce load", "settings.app.form.wakeUpHibernationStrategy": "Hibernation strategy after automatic wake up", "settings.app.form.wakeUpStrategy": "Wake up strategy", @@ -428,6 +429,7 @@ "settings.workspace.add.form.submitButton": "Create workspace", "settings.workspace.form.buttonDelete": "Delete workspace", "settings.workspace.form.buttonSave": "Save workspace", + "settings.workspace.form.iconUrl": "Workspace icon url", "settings.workspace.form.keepLoaded": "Keep this workspace loaded*", "settings.workspace.form.keepLoadedInfo": "*This option will be overwritten by the global \"Keep all workspaces loaded\" option.", "settings.workspace.form.name": "Name", @@ -499,4 +501,4 @@ "workspaceDrawer.workspaceFeatureInfo": "

Ferdium Workspaces let you focus on what’s important right now. Set up different sets of services and easily switch between them at any time.

You decide which services you need when and where, so we can help you stay on top of your game - or easily switch off from work whenever you want.

", "workspaceDrawer.workspacesSettingsTooltip": "Edit workspaces settings", "workspaces.switchingIndicator.switchingTo": "Switching to" -} \ No newline at end of file +} diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index e08e578b1b..f8f5a6c8af 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -263,6 +263,7 @@ "settings.app.form.useGrayscaleServices": "Mettre les icônes de service en noir et blanc", "settings.app.form.useHorizontalStyle": "Utiliser le style horizontal", "settings.app.form.useTouchIdToUnlock": "Autoriser l'utilisation du système de lecture d'empreinte digitale pour déverrouiller Ferdium", + "settings.app.form.useWorkspaceDrawerIconStyle": "Utiliser le style d'affichage icône de l'espace de travail", "settings.app.form.wakeUpHibernationSplay": "Ajuster les cycles de mise en veille prolongée et de réveil afin de réduire la charge", "settings.app.form.wakeUpHibernationStrategy": "Stratégie de mise en veille prolongée après le réveil automatique", "settings.app.form.wakeUpStrategy": "Stratégie de réveil", @@ -423,6 +424,7 @@ "settings.workspace.add.form.submitButton": "Créer l'espace de travail", "settings.workspace.form.buttonDelete": "Effacer l'espace de travail", "settings.workspace.form.buttonSave": "Sauvegarder l'espace de travail", + "settings.workspace.form.iconUrl": "Url de l'icône de l'espace de travail", "settings.workspace.form.keepLoaded": "Garder cet espace de travail chargé*", "settings.workspace.form.keepLoadedInfo": "*Cette option va être écrasée par l'option globale « Garder cette espace de travail chargé ».", "settings.workspace.form.name": "Nom", diff --git a/src/internal-server/app/Controllers/Http/WorkspaceController.js b/src/internal-server/app/Controllers/Http/WorkspaceController.js index d271dac122..77173acff5 100644 --- a/src/internal-server/app/Controllers/Http/WorkspaceController.js +++ b/src/internal-server/app/Controllers/Http/WorkspaceController.js @@ -66,15 +66,17 @@ class WorkspaceController { }); } - const data = request.all(); + const toUpdate = request.all(); const { id } = params; + const { name, services, iconUrl } = toUpdate; // Update data in database await Workspace.query() .where('workspaceId', id) .update({ - name: data.name, - services: JSON.stringify(data.services), + name, + services: JSON.stringify(services), + data: JSON.stringify({ name, iconUrl }), }); // Get updated row @@ -82,13 +84,22 @@ class WorkspaceController { .where('workspaceId', id) .fetch(); const workspace = workspaceQuery.rows[0]; - + let data = {}; + try { + data = JSON.parse(workspace.data); + } catch (error) { + console.warn( + `[WorkspaceController] edit ${workspace.workspaceId}. Error parsing data JSON`, + error, + ); + } return response.send({ id: workspace.workspaceId, name: data.name, order: workspace.order, services: data.services, userId: 1, + iconUrl: data?.iconUrl || '', }); } @@ -122,13 +133,25 @@ class WorkspaceController { // Convert to array with all data Franz wants let workspacesArray = []; if (workspaces) { - workspacesArray = workspaces.map(workspace => ({ - id: workspace.workspaceId, - name: workspace.name, - order: workspace.order, - services: convertToJSON(workspace.services), - userId: 1, - })); + workspacesArray = workspaces.map(workspace => { + let data = {}; + try { + data = JSON.parse(workspace.data); + } catch (error) { + console.warn( + `[WorkspaceController] list ${workspace.workspaceId}. Error parsing data JSON`, + error, + ); + } + return { + id: workspace.workspaceId, + name: workspace.name, + iconUrl: data?.iconUrl || '', + order: workspace.order, + services: convertToJSON(workspace.services), + userId: 1, + }; + }); } return response.send(workspacesArray);