diff --git a/CHANGELOG.md b/CHANGELOG.md index 10be9b1f..45aac6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to ## Added +- Several new blockable services ([#3972]). Those will now be more in sync with + the services that are already blockable in AdGuard DNS. +- A new HTTP API, `GET /control/blocked_services/all`, that lists all available + blocked services and their data, such as SVG icons ([#3972]). - The new optional `tls.override_tls_ciphers` property, which allows overriding TLS ciphers used by AdGuard Home ([#4925], [#4990]). - The ability to serve DNS on link-local IPv6 addresses ([#2926]). @@ -28,6 +32,11 @@ and this project adheres to - Responses with `SERVFAIL` code are now cached for at least 30 seconds. +### Deprecated + +- The `GET /control/blocked_services/services` HTTP API; use the new + `GET /control/blocked_services/all` API instead ([#3972]). + ### Fixed - ClientIDs not working when using DNS-over-HTTPS with HTTP/3. @@ -41,6 +50,7 @@ and this project adheres to [#2926]: https://github.com/AdguardTeam/AdGuardHome/issues/2926 [#3418]: https://github.com/AdguardTeam/AdGuardHome/issues/3418 +[#3972]: https://github.com/AdguardTeam/AdGuardHome/issues/3972 [#4916]: https://github.com/AdguardTeam/AdGuardHome/issues/4916 [#4925]: https://github.com/AdguardTeam/AdGuardHome/issues/4925 [#4942]: https://github.com/AdguardTeam/AdGuardHome/issues/4942 diff --git a/client/src/actions/services.js b/client/src/actions/services.js index 650aa330..f360081e 100644 --- a/client/src/actions/services.js +++ b/client/src/actions/services.js @@ -32,6 +32,21 @@ export const getBlockedServices = () => async (dispatch) => { } }; +export const getAllBlockedServicesRequest = createAction('GET_ALL_BLOCKED_SERVICES_REQUEST'); +export const getAllBlockedServicesFailure = createAction('GET_ALL_BLOCKED_SERVICES_FAILURE'); +export const getAllBlockedServicesSuccess = createAction('GET_ALL_BLOCKED_SERVICES_SUCCESS'); + +export const getAllBlockedServices = () => async (dispatch) => { + dispatch(getAllBlockedServicesRequest()); + try { + const data = await apiClient.getAllBlockedServices(); + dispatch(getAllBlockedServicesSuccess(data)); + } catch (error) { + dispatch(addErrorToast({ error })); + dispatch(getAllBlockedServicesFailure()); + } +}; + export const setBlockedServicesRequest = createAction('SET_BLOCKED_SERVICES_REQUEST'); export const setBlockedServicesFailure = createAction('SET_BLOCKED_SERVICES_FAILURE'); export const setBlockedServicesSuccess = createAction('SET_BLOCKED_SERVICES_SUCCESS'); diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 036f9050..bc030fa1 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -465,11 +465,18 @@ class Api { BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' }; + BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' }; + getBlockedServicesAvailableServices() { const { path, method } = this.BLOCKED_SERVICES_SERVICES; return this.makeRequest(path, method); } + getAllBlockedServices() { + const { path, method } = this.BLOCKED_SERVICES_ALL; + return this.makeRequest(path, method); + } + getBlockedServices() { const { path, method } = this.BLOCKED_SERVICES_LIST; return this.makeRequest(path, method); diff --git a/client/src/components/Filters/Services/Form.js b/client/src/components/Filters/Services/Form.js index 4aed810d..1684d5ea 100644 --- a/client/src/components/Filters/Services/Form.js +++ b/client/src/components/Filters/Services/Form.js @@ -6,10 +6,11 @@ import flow from 'lodash/flow'; import { toggleAllServices } from '../../../helpers/helpers'; import { renderServiceField } from '../../../helpers/form'; -import { FORM_NAME, SERVICES } from '../../../helpers/constants'; +import { FORM_NAME } from '../../../helpers/constants'; const Form = (props) => { const { + blockedServices, handleSubmit, change, pristine, @@ -27,7 +28,7 @@ const Form = (props) => { type="button" className="btn btn-secondary btn-block" disabled={processing || processingSet} - onClick={() => toggleAllServices(SERVICES, change, true)} + onClick={() => toggleAllServices(blockedServices, change, true)} > block_all @@ -37,17 +38,17 @@ const Form = (props) => { type="button" className="btn btn-secondary btn-block" disabled={processing || processingSet} - onClick={() => toggleAllServices(SERVICES, change, false)} + onClick={() => toggleAllServices(blockedServices, change, false)} > unblock_all
- {SERVICES.map((service) => ( + {blockedServices.map((service) => ( { }; Form.propTypes = { + blockedServices: PropTypes.array.isRequired, pristine: PropTypes.bool.isRequired, handleSubmit: PropTypes.func.isRequired, change: PropTypes.func.isRequired, diff --git a/client/src/components/Filters/Services/index.js b/client/src/components/Filters/Services/index.js index be5516bd..09cdd7c8 100644 --- a/client/src/components/Filters/Services/index.js +++ b/client/src/components/Filters/Services/index.js @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import Form from './Form'; import Card from '../../ui/Card'; -import { getBlockedServices, setBlockedServices } from '../../../actions/services'; +import { getBlockedServices, getAllBlockedServices, setBlockedServices } from '../../../actions/services'; import PageTitle from '../../ui/PageTitle'; const getInitialDataForServices = (initial) => (initial ? initial.reduce( @@ -21,6 +21,7 @@ const Services = () => { useEffect(() => { dispatch(getBlockedServices()); + dispatch(getAllBlockedServices()); }, []); const handleSubmit = (values) => { @@ -49,6 +50,7 @@ const Services = () => {
state.filtering.filters, shallowEqual); const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual); const isDetailed = useSelector((state) => state.queryLogs.isDetailed); + const services = useSelector((store) => store?.services); const formattedElapsedMs = formatElapsedMs(elapsedMs, t); @@ -60,8 +61,8 @@ const ResponseCell = ({ install_settings_dns: upstreamString, elapsed: formattedElapsedMs, response_code: status, - ...(service_name - && { service_name: getServiceName(service_name) } + ...(service_name && services.allServices + && { service_name: getServiceName(services.allServices, service_name) } ), ...(rules.length > 0 && { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) } @@ -80,10 +81,10 @@ const ResponseCell = ({ const getDetailedInfo = (reason) => { switch (reason) { case FILTERED_STATUS.FILTERED_BLOCKED_SERVICE: - if (!service_name) { + if (!service_name || !services.allServices) { return formattedElapsedMs; } - return getServiceName(service_name); + return getServiceName(services.allServices, service_name); case FILTERED_STATUS.FILTERED_BLACK_LIST: case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST: return getFilterNames(rules, filters, whitelistFilters).join(', '); diff --git a/client/src/components/Logs/Cells/index.js b/client/src/components/Logs/Cells/index.js index aaffeb07..fe32b104 100644 --- a/client/src/components/Logs/Cells/index.js +++ b/client/src/components/Logs/Cells/index.js @@ -52,6 +52,7 @@ const Row = memo(({ const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual); const processingSet = useSelector((state) => state.access.processingSet); const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual); + const services = useSelector((store) => store?.services); const clients = useSelector((state) => state.dashboard.clients); @@ -175,8 +176,8 @@ const Row = memo(({ date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS), encryption_status: isBlocked ?
{requestStatus}
: requestStatus, - ...(FILTERED_STATUS.FILTERED_BLOCKED_SERVICE && service_name - && { service_name: getServiceName(service_name) }), + ...(FILTERED_STATUS.FILTERED_BLOCKED_SERVICE && service_name && services.allServices + && { service_name: getServiceName(services.allServices, service_name) }), domain, type_table_header: type, protocol, diff --git a/client/src/components/Logs/index.js b/client/src/components/Logs/index.js index e9ec100b..b4b4c636 100644 --- a/client/src/components/Logs/index.js +++ b/client/src/components/Logs/index.js @@ -16,6 +16,7 @@ import { getFilteringStatus } from '../../actions/filtering'; import { getClients } from '../../actions'; import { getDnsConfig } from '../../actions/dnsConfig'; import { getAccessList } from '../../actions/access'; +import { getAllBlockedServices } from '../../actions/services'; import { getLogsConfig, resetFilteredLogs, @@ -130,6 +131,7 @@ const Logs = () => { setIsLoading(true); dispatch(getFilteringStatus()); dispatch(getClients()); + dispatch(getAllBlockedServices()); try { await Promise.all([ dispatch(getLogsConfig()), diff --git a/client/src/components/Settings/Clients/ClientsTable.js b/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js similarity index 56% rename from client/src/components/Settings/Clients/ClientsTable.js rename to client/src/components/Settings/Clients/ClientsTable/ClientsTable.js index 2f08eb61..f83c44ab 100644 --- a/client/src/components/Settings/Clients/ClientsTable.js +++ b/client/src/components/Settings/Clients/ClientsTable/ClientsTable.js @@ -1,25 +1,57 @@ -import React, { Component, Fragment } from 'react'; +/* eslint-disable react/display-name */ +/* eslint-disable react/prop-types */ +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Trans, withTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; import ReactTable from 'react-table'; -import { MODAL_TYPE } from '../../../helpers/constants'; -import { splitByNewLine, countClientsStatistics, sortIp } from '../../../helpers/helpers'; -import Card from '../../ui/Card'; -import Modal from './Modal'; -import CellWrap from '../../ui/CellWrap'; -import LogsSearchLink from '../../ui/LogsSearchLink'; +import { getAllBlockedServices } from '../../../../actions/services'; +import { + splitByNewLine, + countClientsStatistics, + sortIp, + getService, +} from '../../../../helpers/helpers'; +import { MODAL_TYPE } from '../../../../helpers/constants'; +import Card from '../../../ui/Card'; +import CellWrap from '../../../ui/CellWrap'; +import LogsSearchLink from '../../../ui/LogsSearchLink'; +import Modal from '../Modal'; -class ClientsTable extends Component { - handleFormAdd = (values) => { - this.props.addClient(values); +const ClientsTable = ({ + clients, + normalizedTopClients, + isModalOpen, + modalClientName, + modalType, + addClient, + updateClient, + deleteClient, + toggleClientModal, + processingAdding, + processingDeleting, + processingUpdating, + getStats, + supportedTags, +}) => { + const [t] = useTranslation(); + const dispatch = useDispatch(); + const services = useSelector((store) => store?.services); + + useEffect(() => { + dispatch(getAllBlockedServices()); + }, []); + + const handleFormAdd = (values) => { + addClient(values); }; - handleFormUpdate = (values, name) => { - this.props.updateClient(values, name); + const handleFormUpdate = (values, name) => { + updateClient(values, name); }; - handleSubmit = (values) => { + const handleSubmit = (values) => { const config = values; if (values) { @@ -42,21 +74,21 @@ class ClientsTable extends Component { } } - if (this.props.modalType === MODAL_TYPE.EDIT_FILTERS) { - this.handleFormUpdate(config, this.props.modalClientName); + if (modalType === MODAL_TYPE.EDIT_FILTERS) { + handleFormUpdate(config, modalClientName); } else { - this.handleFormAdd(config); + handleFormAdd(config); } }; - getOptionsWithLabels = (options) => ( + const getOptionsWithLabels = (options) => ( options.map((option) => ({ value: option, label: option, })) ); - getClient = (name, clients) => { + const getClient = (name, clients) => { const client = clients.find((item) => name === item.name); if (client) { @@ -65,7 +97,7 @@ class ClientsTable extends Component { } = client; return { upstreams: (upstreams && upstreams.join('\n')) || '', - tags: (tags && this.getOptionsWithLabels(tags)) || [], + tags: (tags && getOptionsWithLabels(tags)) || [], ...values, }; } @@ -78,17 +110,17 @@ class ClientsTable extends Component { }; }; - handleDelete = (data) => { + const handleDelete = (data) => { // eslint-disable-next-line no-alert - if (window.confirm(this.props.t('client_confirm_delete', { key: data.name }))) { - this.props.deleteClient(data); - this.props.getStats(); + if (window.confirm(t('client_confirm_delete', { key: data.name }))) { + deleteClient(data); + getStats(); } }; - columns = [ + const columns = [ { - Header: this.props.t('table_client'), + Header: t('table_client'), accessor: 'ids', minWidth: 150, Cell: (row) => { @@ -109,13 +141,13 @@ class ClientsTable extends Component { sortMethod: sortIp, }, { - Header: this.props.t('table_name'), + Header: t('table_name'), accessor: 'name', minWidth: 120, Cell: CellWrap, }, { - Header: this.props.t('settings'), + Header: t('settings'), accessor: 'use_global_settings', minWidth: 120, Cell: ({ value }) => { @@ -133,7 +165,7 @@ class ClientsTable extends Component { }, }, { - Header: this.props.t('blocked_services'), + Header: t('blocked_services'), accessor: 'blocked_services', minWidth: 180, Cell: (row) => { @@ -143,25 +175,40 @@ class ClientsTable extends Component { return settings_global; } + if (value && services.allServices) { + return ( +
+ {value.map((service) => { + const serviceInfo = getService(services.allServices, service); + + if (serviceInfo?.icon_svg) { + return ( +
+ ); + } + + return null; + })} +
+ ); + } + return (
- {value && value.length > 0 - ? value.map((service) => ( - - - - )) - : '–'} + –
); }, }, { - Header: this.props.t('upstreams'), + Header: t('upstreams'), accessor: 'upstreams', minWidth: 120, Cell: ({ value }) => { @@ -179,7 +226,7 @@ class ClientsTable extends Component { }, }, { - Header: this.props.t('tags_title'), + Header: t('tags_title'), accessor: 'tags', minWidth: 140, Cell: (row) => { @@ -203,11 +250,11 @@ class ClientsTable extends Component { }, }, { - Header: this.props.t('requests_count'), + Header: t('requests_count'), id: 'statistics', accessor: (row) => countClientsStatistics( row.ids, - this.props.normalizedTopClients.auto, + normalizedTopClients.auto, ), sortMethod: (a, b) => b - a, minWidth: 120, @@ -222,16 +269,13 @@ class ClientsTable extends Component { }, }, { - Header: this.props.t('actions_table_header'), + Header: t('actions_table_header'), accessor: 'actions', maxWidth: 100, sortable: false, resizable: false, Cell: (row) => { const clientName = row.original.name; - const { - toggleClientModal, processingDeleting, processingUpdating, t, - } = this.props; return (
@@ -253,7 +297,7 @@ class ClientsTable extends Component { - - - - ); - } -} + return ( + + <> + + + + + + ); +}; ClientsTable.propTypes = { - t: PropTypes.func.isRequired, clients: PropTypes.array.isRequired, normalizedTopClients: PropTypes.object.isRequired, toggleClientModal: PropTypes.func.isRequired, @@ -353,4 +382,4 @@ ClientsTable.propTypes = { supportedTags: PropTypes.array.isRequired, }; -export default withTranslation()(ClientsTable); +export default ClientsTable; diff --git a/client/src/components/Settings/Clients/ClientsTable/index.js b/client/src/components/Settings/Clients/ClientsTable/index.js new file mode 100644 index 00000000..fdbe7807 --- /dev/null +++ b/client/src/components/Settings/Clients/ClientsTable/index.js @@ -0,0 +1 @@ +export { default as ClientsTable } from './ClientsTable'; diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index 353b0bea..35d9764d 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { connect } from 'react-redux'; +import { connect, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { Field, FieldArray, reduxForm, formValueSelector, @@ -19,7 +19,7 @@ import { renderServiceField, } from '../../../helpers/form'; import { validateClientId, validateRequiredValue } from '../../../helpers/validators'; -import { CLIENT_ID_LINK, FORM_NAME, SERVICES } from '../../../helpers/constants'; +import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants'; import './Service.css'; const settingsCheckboxes = [ @@ -139,6 +139,7 @@ let Form = (props) => { invalid, tagsOptions, } = props; + const services = useSelector((store) => store?.services); const [activeTabLabel, setActiveTabLabel] = useState('settings'); @@ -180,7 +181,9 @@ let Form = (props) => { type="button" className="btn btn-secondary btn-block" disabled={useGlobalServices} - onClick={() => toggleAllServices(SERVICES, change, true)} + onClick={() => ( + toggleAllServices(services.allServices, change, true) + )} > block_all @@ -190,25 +193,29 @@ let Form = (props) => { type="button" className="btn btn-secondary btn-block" disabled={useGlobalServices} - onClick={() => toggleAllServices(SERVICES, change, false)} + onClick={() => ( + toggleAllServices(services.allServices, change, false) + )} > unblock_all
-
- {SERVICES.map((service) => ( - - ))} -
+ {services.allServices.length > 0 && ( +
+ {services.allServices.map((service) => ( + + ))} +
+ )}
, }, diff --git a/client/src/components/Settings/Clients/Service.css b/client/src/components/Settings/Clients/Service.css index 7c1890fe..153cf2d8 100644 --- a/client/src/components/Settings/Clients/Service.css +++ b/client/src/components/Settings/Clients/Service.css @@ -9,6 +9,12 @@ cursor: pointer; } +.service__text { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + @media screen and (min-width: 768px) { .services { display: flex; @@ -33,7 +39,7 @@ margin-right: 30px; margin-left: 0; } - + .service:nth-child(3n) { margin-right: 0; margin-left: auto; diff --git a/client/src/components/Settings/Clients/index.js b/client/src/components/Settings/Clients/index.js index a6dfc0b1..b7c50ae8 100644 --- a/client/src/components/Settings/Clients/index.js +++ b/client/src/components/Settings/Clients/index.js @@ -2,7 +2,7 @@ import React, { Component, Fragment } from 'react'; import { withTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; -import ClientsTable from './ClientsTable'; +import { ClientsTable } from './ClientsTable'; import AutoClients from './AutoClients'; import PageTitle from '../../ui/PageTitle'; import Loading from '../../ui/Loading'; diff --git a/client/src/components/ui/Icons.js b/client/src/components/ui/Icons.js index 47e71f59..38eff1be 100644 --- a/client/src/components/ui/Icons.js +++ b/client/src/components/ui/Icons.js @@ -98,157 +98,6 @@ const Icons = () => ( d="M15 3C10.57 3 6.701 5.419 4.623 9h2.39a10.063 10.063 0 0 1 4.05-3.19c-.524.89-.961 1.973-1.3 3.19h2.108c.79-2.459 1.998-4 3.129-4s2.339 1.541 3.129 4h2.107c-.338-1.217-.774-2.3-1.299-3.19A10.062 10.062 0 0 1 22.989 9h2.389C23.298 5.419 19.43 3 15 3zm7.035 9.129c-1.372 0-2.264.73-2.264 1.842 0 .896.538 1.463 1.579 1.66l.75.15c.65.13.898.3.898.615 0 .375-.37.635-.91.635-.6 0-1.014-.265-1.049-.68h-1.38c.023 1.097.93 1.776 2.37 1.776 1.491 0 2.399-.717 2.399-1.904 0-.903-.504-1.412-1.63-1.63l-.734-.142c-.6-.118-.851-.3-.851-.611 0-.378.336-.62.844-.62.509 0 .891.28.923.682h1.336c-.024-1.053-.948-1.773-2.28-1.773zm-16.185.148v5.696h2.39c1.712 0 2.662-1.033 2.662-2.903 0-1.779-.966-2.793-2.662-2.793H5.85zm6.933.004v5.692h1.373v-3.235h.076l2.377 3.235h1.149V12.28h-1.373v3.203h-.076l-2.372-3.203h-1.154zm-5.486 1.16h.682c.912 0 1.449.596 1.449 1.657 0 1.128-.51 1.713-1.45 1.713h-.681v-3.37zM4.623 21C6.701 24.581 10.57 27 15 27c4.43 0 8.299-2.419 10.377-6h-2.389a10.063 10.063 0 0 1-4.049 3.19c.524-.89.96-1.973 1.297-3.19H18.13c-.79 2.459-1.996 4-3.127 4-1.131 0-2.339-1.541-3.129-4h-2.11c.339 1.217.776 2.3 1.3 3.19A10.056 10.056 0 0 1 7.013 21h-2.39z"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -356,62 +205,6 @@ const Icons = () => ( d="M8.036 10.93l3.93 4.07 4.068-3.93" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); diff --git a/client/src/components/ui/Tabler.css b/client/src/components/ui/Tabler.css index 34d37622..fac3d80a 100644 --- a/client/src/components/ui/Tabler.css +++ b/client/src/components/ui/Tabler.css @@ -15452,6 +15452,7 @@ a.tag-addon:hover { } .custom-switch-indicator { + flex-shrink: 0; display: inline-block; height: 1.25rem; width: 2.25rem; diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 943b28ac..c13ab952 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -202,158 +202,6 @@ export const FILTERS_URLS = { blocked_services: '/blocked_services', }; -export const SERVICES = [ - { - id: '9gag', - name: '9GAG', - }, - { - id: 'amazon', - name: 'Amazon', - }, - { - id: 'bilibili', - name: 'Bilibili', - }, - { - id: 'cloudflare', - name: 'CloudFlare', - }, - { - id: 'dailymotion', - name: 'Dailymotion', - }, - { - id: 'discord', - name: 'Discord', - }, - { - id: 'disneyplus', - name: 'Disney+', - }, - { - id: 'ebay', - name: 'EBay', - }, - { - id: 'epic_games', - name: 'Epic Games', - }, - { - id: 'facebook', - name: 'Facebook', - }, - { - id: 'hulu', - name: 'Hulu', - }, - { - id: 'imgur', - name: 'Imgur', - }, - { - id: 'instagram', - name: 'Instagram', - }, - { - id: 'mail_ru', - name: 'Mail.ru', - }, - { - id: 'netflix', - name: 'Netflix', - }, - { - id: 'ok', - name: 'OK.ru', - }, - { - id: 'origin', - name: 'Origin', - }, - { - id: 'pinterest', - name: 'Pinterest', - }, - { - id: 'qq', - name: 'QQ', - }, - { - id: 'reddit', - name: 'Reddit', - }, - { - id: 'skype', - name: 'Skype', - }, - { - id: 'snapchat', - name: 'Snapchat', - }, - { - id: 'spotify', - name: 'Spotify', - }, - { - id: 'steam', - name: 'Steam', - }, - { - id: 'telegram', - name: 'Telegram', - }, - { - id: 'tiktok', - name: 'TikTok', - }, - { - id: 'tinder', - name: 'Tinder', - }, - { - id: 'twitch', - name: 'Twitch', - }, - { - id: 'twitter', - name: 'Twitter', - }, - { - id: 'viber', - name: 'Viber', - }, - { - id: 'vimeo', - name: 'Vimeo', - }, - { - id: 'vk', - name: 'VK.com', - }, - { - id: 'wechat', - name: 'WeChat', - }, - { - id: 'weibo', - name: 'Weibo', - }, - { - id: 'whatsapp', - name: 'WhatsApp', - }, - { - id: 'youtube', - name: 'YouTube', - }, -]; - -export const SERVICES_ID_NAME_MAP = SERVICES.reduce((acc, { id, name }) => { - acc[id] = name; - return acc; -}, {}); - export const ENCRYPTION_SOURCE = { PATH: 'path', CONTENT: 'content', diff --git a/client/src/helpers/form.js b/client/src/helpers/form.js index c4f90722..f58aa830 100644 --- a/client/src/helpers/form.js +++ b/client/src/helpers/form.js @@ -1,6 +1,8 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { Trans } from 'react-i18next'; +import cn from 'classnames'; + import { createOnBlurHandler } from './helpers'; import { R_MAC_WITHOUT_COLON, R_UNIX_ABSOLUTE_PATH, R_WIN_ABSOLUTE_PATH } from './constants'; @@ -229,24 +231,34 @@ export const renderServiceField = ({ modifier, icon, meta: { touched, error }, -}) => - - {!disabled && touched && error - && {error}} -; +}) => ( + <> +