diff --git a/app/assets/locales/en.json b/app/assets/locales/en.json index 60a29c0d66..c701b5cb7c 100644 --- a/app/assets/locales/en.json +++ b/app/assets/locales/en.json @@ -164,6 +164,10 @@ "wrong_access_code": "Wrong Access Code", "generate_viewers_access_code": "Generate access code for viewers", "generate_mods_access_code": "Generate access code for moderators", + "server_tag": "Select a server type for this room", + "default_tag_name": "Default", + "server_tag_desired": "Desired", + "server_tag_required": "Required", "are_you_sure_delete_room": "Are you sure you want to delete this room?" } }, @@ -274,8 +278,8 @@ "administration": { "administration": "Administration", "terms": "Terms & Conditions", - "privacy": "Privacy Policy", - "privacy_policy": "Privacy Policy", + "privacy": "Privacy Notice", + "privacy_policy": "Privacy Notice", "change_term_links": "Change the terms links that appears at the bottom of the page", "change_privacy_link": "Change the privacy link that appears at the bottom of the page", "helpcenter": "Help Center", @@ -413,7 +417,7 @@ "brand_color_updated": "The brand color has been updated.", "brand_image_updated": "The brand image has been updated.", "brand_image_deleted": "The brand image has been deleted.", - "privacy_policy_updated": "The privacy policy has been updated.", + "privacy_policy_updated": "The privacy notice has been updated.", "helpcenter_updated": "The help center link has been updated.", "terms_of_service_updated": "The terms of service have been updated.", "maintenance_updated": "The maintenance banner has been updated." @@ -437,6 +441,7 @@ }, "error": { "problem_completing_action": "The action can't be completed. \n Please try again.", + "server_type_unavailable": "The required server type is unavailable. Please select a different type in the room settings.", "file_type_not_supported": "The file type is not supported.", "file_size_too_large": "The file size is too large.", "file_upload_error": "The file can't be uploaded.", @@ -533,6 +538,11 @@ }, "url": { "invalid": "Invalid URL" + }, + "text_form": { + "value": { + "required": "Please enter some message" + } } }, "room": { diff --git a/app/controllers/api/v1/locales_controller.rb b/app/controllers/api/v1/locales_controller.rb index 911f2d73af..f86af3b2eb 100644 --- a/app/controllers/api/v1/locales_controller.rb +++ b/app/controllers/api/v1/locales_controller.rb @@ -48,11 +48,13 @@ def index # Returns the requested language's locale strings (returns 406 if locale doesn't exist) def show language = params[:name].tr('-', '_') + language_file = Dir.entries('app/assets/locales').select { |f| f.starts_with?(language) } + final_language = language_file.min&.gsub('.json', '') # Serve locales files directly in development (not through asset pipeline) - return render file: Rails.root.join('app', 'assets', 'locales', "#{language}.json") if Rails.env.development? + return render file: Rails.root.join('app', 'assets', 'locales', "#{final_language}.json") if Rails.env.development? - redirect_to ActionController::Base.helpers.asset_path("#{language}.json") + redirect_to ActionController::Base.helpers.asset_path("#{final_language}.json") rescue StandardError head :not_acceptable end diff --git a/app/controllers/api/v1/meetings_controller.rb b/app/controllers/api/v1/meetings_controller.rb index be51924d4b..f0a68b5819 100644 --- a/app/controllers/api/v1/meetings_controller.rb +++ b/app/controllers/api/v1/meetings_controller.rb @@ -31,7 +31,7 @@ def start begin MeetingStarter.new(room: @room, base_url: request.base_url, current_user:, provider: current_provider).call rescue BigBlueButton::BigBlueButtonException => e - return render_error status: :bad_request unless e.key == 'idNotUnique' + return render_error status: :bad_request, errors: e.key unless e.key == 'idNotUnique' end render_data data: BigBlueButtonApi.new(provider: current_provider).join_meeting( diff --git a/app/controllers/api/v1/server_tags_controller.rb b/app/controllers/api/v1/server_tags_controller.rb new file mode 100644 index 0000000000..8692a657f4 --- /dev/null +++ b/app/controllers/api/v1/server_tags_controller.rb @@ -0,0 +1,37 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with Greenlight; if not, see . + +# frozen_string_literal: true + +module Api + module V1 + class ServerTagsController < ApiController + # GET /api/v1/server_tags/:friendly_id + # Returns a list of all allowed tags&names for the room's owner + def show + tag_names = Rails.configuration.server_tag_names + tag_roles = Rails.configuration.server_tag_roles + return render_data data: {}, status: :ok if tag_names.blank? + + room = Room.find_by(friendly_id: params[:friendly_id]) + return render_data data: {}, status: :ok if room.nil? + + allowed_tag_names = tag_names.reject { |tag, _| tag_roles.key?(tag) && tag_roles[tag].exclude?(room.user.role_id) } + render_data data: allowed_tag_names, status: :ok + end + end + end +end diff --git a/app/javascript/components/admin/site_settings/administration/TextForm.jsx b/app/javascript/components/admin/site_settings/administration/TextForm.jsx index ff2567fb2a..4e17e8e51c 100644 --- a/app/javascript/components/admin/site_settings/administration/TextForm.jsx +++ b/app/javascript/components/admin/site_settings/administration/TextForm.jsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License along // with Greenlight; if not, see . -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; @@ -25,39 +25,80 @@ import FormControl from '../../../shared_components/forms/FormControl'; import useTextForm from '../../../../hooks/forms/admin/site_settings/useTextForm'; export default function TextForm({ id, value, mutation: useUpdateSiteSettingsAPI }) { - const updateSiteSettingsAPI = useUpdateSiteSettingsAPI(); + const updateSiteSettingsAPISetText = useUpdateSiteSettingsAPI(); + const updateSiteSettingsAPIClearText = useUpdateSiteSettingsAPI(); + const { t } = useTranslation(); - const maintenanceBannerId = localStorage.getItem('maintenanceBannerId'); const { methods, fields } = useTextForm({ defaultValues: { value } }); + const formText = useRef(''); + useEffect(() => { - if (!methods) { return; } - methods.reset({ value }); + if (methods) { + methods.reset({ value }); + formText.current = value; + } }, [methods, value]); + const dismissMaintenanceBannerToast = () => { + const maintenanceBannerId = localStorage.getItem('maintenanceBannerId'); + if (maintenanceBannerId) { + toast.dismiss(maintenanceBannerId); + localStorage.removeItem('maintenanceBannerId'); + } + }; + // Function to clear the form const clearForm = () => { methods.reset({ value: '' }); - toast.dismiss(maintenanceBannerId); - updateSiteSettingsAPI.mutate(''); + dismissMaintenanceBannerToast(); + if (formText.current) { + formText.current = ''; + updateSiteSettingsAPIClearText.mutate(''); + } }; + const handleSubmit = useCallback((formData) => { + if (formText.current !== formData[`${fields.value.hookForm.id}`]) { + dismissMaintenanceBannerToast(); + formText.current = formData[`${fields.value.hookForm.id}`]; + return updateSiteSettingsAPISetText.mutate(formData); + } + return null; + }, [updateSiteSettingsAPISetText.mutate]); + return ( -
+ - - ); diff --git a/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx b/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx index 159f8ed303..973ca6afc0 100644 --- a/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx +++ b/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx @@ -33,6 +33,8 @@ import { useAuth } from '../../../../contexts/auth/AuthProvider'; import UpdateRoomNameForm from './forms/UpdateRoomNameForm'; import useRoom from '../../../../hooks/queries/rooms/useRoom'; import UnshareRoom from './UnshareRoom'; +import useServerTags from '../../../../hooks/queries/rooms/useServerTags'; +import ServerTagRow from './ServerTagRow'; export default function RoomSettings() { const { t } = useTranslation(); @@ -41,6 +43,7 @@ export default function RoomSettings() { const roomSetting = useRoomSettings(friendlyId); const { data: roomConfigs } = useRoomConfigs(); const { data: room } = useRoom(friendlyId); + const { data: serverTags } = useServerTags(friendlyId); const updateMutationWrapper = () => useUpdateRoomSetting(friendlyId); const deleteMutationWrapper = (args) => useDeleteRoom({ friendlyId, ...args }); @@ -66,6 +69,15 @@ export default function RoomSettings() { config={roomConfigs?.glModeratorAccessCode} description={t('room.settings.generate_mods_access_code')} /> + {serverTags && Object.keys(serverTags).length !== 0 && ( + + )}
{ t('room.settings.user_settings') }
diff --git a/app/javascript/components/rooms/room/room_settings/ServerTagRow.jsx b/app/javascript/components/rooms/room/room_settings/ServerTagRow.jsx new file mode 100644 index 0000000000..044df49710 --- /dev/null +++ b/app/javascript/components/rooms/room/room_settings/ServerTagRow.jsx @@ -0,0 +1,118 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { + Row, Col, Dropdown, ButtonGroup, ToggleButton, +} from 'react-bootstrap'; +import SimpleSelect from '../../../shared_components/utilities/SimpleSelect'; + +export default function ServerTagRow({ + updateMutation: useUpdateAPI, currentTag, tagRequired, serverTags, description, +}) { + const updateAPI = useUpdateAPI(); + const { t } = useTranslation(); + + function getDefaultTagName() { + return t('room.settings.default_tag_name'); + } + + function getTagName(tag) { + if (tag in serverTags) { + return serverTags[tag]; + } + return getDefaultTagName(); + } + + const dropdownTags = Object.entries(serverTags).map(([tagString, tagName]) => ( + ( + updateAPI.mutate({ settingName: 'serverTag', settingValue: tagString })} + > + {tagName} + + ) + )); + + return ( + +
{description}
+ + + {[ + updateAPI.mutate({ settingName: 'serverTag', settingValue: '' })} + > + {getDefaultTagName()} + , + ].concat(dropdownTags)} + + + + + { + updateAPI.mutate({ settingName: 'serverTagRequired', settingValue: false }); + }} + > + {t('room.settings.server_tag_desired')} + + { + updateAPI.mutate({ settingName: 'serverTagRequired', settingValue: true }); + }} + > + {t('room.settings.server_tag_required')} + + + +
+ ); +} + +ServerTagRow.defaultProps = { + currentTag: '', + tagRequired: false, +}; + +ServerTagRow.propTypes = { + updateMutation: PropTypes.func.isRequired, + currentTag: PropTypes.string, + tagRequired: PropTypes.bool, + serverTags: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + description: PropTypes.string.isRequired, +}; diff --git a/app/javascript/components/shared_components/Footer.jsx b/app/javascript/components/shared_components/Footer.jsx index 30a3b5b2b4..2b30506e3c 100644 --- a/app/javascript/components/shared_components/Footer.jsx +++ b/app/javascript/components/shared_components/Footer.jsx @@ -19,17 +19,20 @@ import { useTranslation } from 'react-i18next'; import { Container } from 'react-bootstrap'; import useEnv from '../../hooks/queries/env/useEnv'; import useSiteSetting from '../../hooks/queries/site_settings/useSiteSetting'; +import { useAuth } from '../../contexts/auth/AuthProvider'; export default function Footer() { const { t } = useTranslation(); const { data: env } = useEnv(); const { data: links } = useSiteSetting(['Terms', 'PrivacyPolicy']); + const currentUser = useAuth(); + const isAdmin = currentUser && currentUser.role && currentUser?.role.name === 'Administrator'; return (