From 13c5e160211a97d4a169059a0e7a7bbd61763a5f Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Sun, 19 Nov 2023 20:17:55 -0300 Subject: [PATCH 01/20] feat: Settings page --- .../components/WelcomeUser/WelcomeUser.tsx | 10 +- src/pages/Settings/Settings.less | 92 +++++++++++++++++++ src/pages/Settings/Settings.tsx | 29 ++++++ .../SettingsOptions/SettingsMoveTo.tsx | 21 +++++ .../Settings/SettingsTitle/SettingsTitle.tsx | 15 +++ src/pages/Settings/index.ts | 5 + src/services/routes.tsx | 5 + src/types/User.ts | 3 + 8 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/pages/Settings/Settings.less create mode 100644 src/pages/Settings/Settings.tsx create mode 100644 src/pages/Settings/SettingsOptions/SettingsMoveTo.tsx create mode 100644 src/pages/Settings/SettingsTitle/SettingsTitle.tsx create mode 100644 src/pages/Settings/index.ts diff --git a/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx b/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx index b4290116..49f7c145 100644 --- a/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx +++ b/src/components/UniversiHeader/components/WelcomeUser/WelcomeUser.tsx @@ -1,5 +1,5 @@ import { useContext, useMemo, useState } from "react"; -import { Link, redirect } from "react-router-dom"; +import { Link, redirect, useNavigate } from "react-router-dom"; import { ProfileImage } from "@/components/ProfileImage/ProfileImage"; import { AuthContext } from "@/contexts/Auth"; import "./WelcomeUser.less" @@ -8,6 +8,7 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu" export function WelcomeUser() { const auth = useContext(AuthContext); + const navigate = useNavigate(); const [profileClicked, setProfileClicked] = useState(false) @@ -24,10 +25,13 @@ export function WelcomeUser() { profileClicked ?
-
{location.href="/profile/"+auth.profile?.user.name}}> +
{navigate("/profile/"+auth.profile?.user.name); setProfileClicked(false)}}> Perfil
-
{auth.signout()}}> + { auth.user?.accessLevel === "ROLE_ADMIN" &&
{navigate("/settings"); setProfileClicked(false)}}> + Configurações +
} +
{auth.signout(); setProfileClicked(false)}}> Sair
diff --git a/src/pages/Settings/Settings.less b/src/pages/Settings/Settings.less new file mode 100644 index 00000000..2330767e --- /dev/null +++ b/src/pages/Settings/Settings.less @@ -0,0 +1,92 @@ +@import url(/src/layouts/colors.less); +@import url(/src/layouts/fonts.less); +@import url(/src/layouts/border-radius.less); +@import url(/src/layouts/spacing.less); + +#universi-settings { + width: 100%; + + display: flex; + align-items: center; + justify-content: center; + + #settings-content { + min-width: 500px; + max-width: min(75%, 1024px); + + margin: @top-page-margin @left-page-padding; + + border-radius: @border-radius; + background-color: @card-background-color; + border: solid 1px @card-background-color; + + color: @font-color-v2; + + overflow: hidden; + + @option-padding-x: 2rem; + + .settings-page-title { + font-size: 1.5rem; + font-weight: @font-weight-bold; + padding: 1rem (@option-padding-x / 2) .25rem; + + a { + color: inherit; + margin-right: .5em; + } + } + + > .settings-option { + display: flex; + flex-direction: column; + align-items: center; + + background-color: transparent; + + color: inherit; + text-decoration: none; + + cursor: pointer; + + will-change: background-color; + + &:hover { + background-color: @card-item-color; + } + + &:not(:last-child)::after { + content: ""; + background-color: @card-item-color; + width: 100%; + height: 1px; + } + + .settings-option-content { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + padding: 1rem @option-padding-x; + width: 100%; + + .settings-option-info { + .settings-option-title { + font-weight: @font-weight-bold; + font-size: 1.25rem; + } + + .settings-option-description { + font-weight: @font-weight-default; + } + } + + .bi { + margin-left: 2rem; + font-size: 1.5em; + } + } + } + } +} diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx new file mode 100644 index 00000000..3ddb0b58 --- /dev/null +++ b/src/pages/Settings/Settings.tsx @@ -0,0 +1,29 @@ +import { useContext } from "react"; +import { useOutlet } from "react-router"; +import { Navigate } from "react-router-dom"; + +import { SettingsTitle, SettingsMoveTo } from "@/pages/Settings"; +import { AuthContext } from "@/contexts/Auth"; + +import "./Settings.less"; + +export function SettingsPage() { + const outlet = useOutlet(); + const auth = useContext(AuthContext); + + if (auth.profile?.user.accessLevel !== "ROLE_ADMIN") + return ; + + return
+
+ { outlet ?? <> + Configurações + + + + + + } +
+
+} diff --git a/src/pages/Settings/SettingsOptions/SettingsMoveTo.tsx b/src/pages/Settings/SettingsOptions/SettingsMoveTo.tsx new file mode 100644 index 00000000..ddf82d90 --- /dev/null +++ b/src/pages/Settings/SettingsOptions/SettingsMoveTo.tsx @@ -0,0 +1,21 @@ +import { ReactNode } from "react"; +import { Link } from "react-router-dom"; + +export type SettingsMoveToProps = { + title: NonNullable; + description?: ReactNode; + to: string; +}; + +export function SettingsMoveTo(props: Readonly) { + return +
+
+

{ props.title }

+ { props.description &&

{ props.description }

} +
+ + +
+ +} diff --git a/src/pages/Settings/SettingsTitle/SettingsTitle.tsx b/src/pages/Settings/SettingsTitle/SettingsTitle.tsx new file mode 100644 index 00000000..786324b8 --- /dev/null +++ b/src/pages/Settings/SettingsTitle/SettingsTitle.tsx @@ -0,0 +1,15 @@ +import { ReactNode } from "react"; +import { Link } from "react-router-dom"; + +export type SettingsTitleProps = { + children: NonNullable; +}; + +export function SettingsTitle({ children }: Readonly) { + return
+ { location.pathname !== "/settings" && + + } + { children } +
+} diff --git a/src/pages/Settings/index.ts b/src/pages/Settings/index.ts new file mode 100644 index 00000000..1440070c --- /dev/null +++ b/src/pages/Settings/index.ts @@ -0,0 +1,5 @@ +export { SettingsPage as default } from "./Settings"; + +export * from "./Settings"; +export * from "./SettingsTitle/SettingsTitle"; +export * from "./SettingsOptions/SettingsMoveTo"; diff --git a/src/services/routes.tsx b/src/services/routes.tsx index b2ec19e4..b46f486c 100644 --- a/src/services/routes.tsx +++ b/src/services/routes.tsx @@ -20,6 +20,7 @@ import Recovery from "@/pages/Recovery/Recovery"; import NewPassword from "@/pages/NewPassword/NewPassword"; import ManageProfilePage, { ManageProfileLoader } from "@/pages/ManageProfile"; import Homepage from "@/pages/Homepage"; +import SettingsPage from "@/pages/Settings"; @@ -96,6 +97,10 @@ export const router = createBrowserRouter([{ element: , loader: ManageGroupLoader, }, + { + path: "/settings", + element: , + } ] }, ]) diff --git a/src/types/User.ts b/src/types/User.ts index 92be501d..0243ebda 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -1,7 +1,10 @@ +export type UserAccessLevel = "ROLE_USER" | "ROLE_DEV" | "ROLE_ADMIN"; + export type User = { id: string; name: string; email?: string; ownerOfSession: boolean; needProfile: boolean; + accessLevel?: UserAccessLevel; } From 9082113172f7d7b76dcf005ba7e982b49435e24f Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Mon, 20 Nov 2023 11:50:45 -0300 Subject: [PATCH 02/20] feat: GroupEmailFilter type and endpoints --- src/services/UniversimeApi/Group.ts | 77 +++++++++++++++++++++++++---- src/types/Group.ts | 8 +++ 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/services/UniversimeApi/Group.ts b/src/services/UniversimeApi/Group.ts index 83386c1a..6c5f4b92 100644 --- a/src/services/UniversimeApi/Group.ts +++ b/src/services/UniversimeApi/Group.ts @@ -1,4 +1,4 @@ -import type { Group, GroupType } from "@/types/Group"; +import type { Group, GroupType, GroupEmailFilter } from "@/types/Group"; import type { Profile } from "@/types/Profile"; import type { ApiResponse } from "@/types/UniversimeApi"; import { api } from "./api"; @@ -39,15 +39,36 @@ export type GroupIdOrPath_RequestDTO = { groupPath?: string; }; -export type GroupGet_ResponseDTO = ApiResponse<{ group: Group }>; -export type GroupCreate_ResponseDTO = ApiResponse; -export type GroupUpdate_ResponseDTO = ApiResponse; -export type GroupAvailableParents_ResponseDTO = ApiResponse<{ groups: Group[] }>; -export type GroupSubgroups_ResponseDTO = ApiResponse<{ subgroups: Group[] }>; -export type GroupParticipants_ResponseDTO = ApiResponse<{ participants: Profile[] }>; -export type GroupJoin_ResponseDTO = ApiResponse; -export type GroupExit_ResponseDTO = ApiResponse; -export type GroupFolders_ResponseDTO = ApiResponse<{ folders: Folder[] }>; +export type GroupEmailFilterAdd_RequestDTO = GroupIdOrPath_RequestDTO & { + email: string; + isEnabled?: boolean; + isRegex?: boolean; +}; + +export type GroupEmailFilterEdit_RequestDTO = GroupIdOrPath_RequestDTO & { + emailFilterId: string; + email?: string; + isEnabled?: boolean; + isRegex?: boolean; +}; + +export type GroupEmailFilterDelete_RequestDTO = GroupIdOrPath_RequestDTO & { + emailFilterId: string; +}; + +export type GroupGet_ResponseDTO = ApiResponse<{ group: Group }>; +export type GroupCreate_ResponseDTO = ApiResponse; +export type GroupUpdate_ResponseDTO = ApiResponse; +export type GroupAvailableParents_ResponseDTO = ApiResponse<{ groups: Group[] }>; +export type GroupSubgroups_ResponseDTO = ApiResponse<{ subgroups: Group[] }>; +export type GroupParticipants_ResponseDTO = ApiResponse<{ participants: Profile[] }>; +export type GroupJoin_ResponseDTO = ApiResponse; +export type GroupExit_ResponseDTO = ApiResponse; +export type GroupFolders_ResponseDTO = ApiResponse<{ folders: Folder[] }>; +export type GroupEmailFilterAdd_ResponseDTO = ApiResponse; +export type GroupEmailFilterEdit_ResponseDTO = ApiResponse; +export type GroupEmailFilterDelete_ResponseDTO = ApiResponse; +export type GroupEmailFilterList_ResponseDTO = ApiResponse<{ emailFilters: GroupEmailFilter[] }>; export async function get(body: GroupIdOrPath_RequestDTO) { return (await api.post('/group/get', { @@ -119,3 +140,39 @@ export async function folders(body: GroupIdOrPath_RequestDTO) { groupPath: body.groupPath, })).data; } + +export async function addEmailFilter(body: GroupEmailFilterAdd_RequestDTO) { + return (await api.post("/group/settings/email-filter/add", { + groupId: body.groupId, + groupPath: body.groupPath, + email: body.email, + enabled: body.isEnabled, + isRegex: body.isRegex, + })).data; +} + +export async function editEmailFilter(body: GroupEmailFilterEdit_RequestDTO) { + return (await api.post("/group/settings/email-filter/edit", { + groupId: body.groupId, + groupPath: body.groupPath, + groupEmailFilterId: body.emailFilterId, + email: body.email, + enabled: body.isEnabled, + isRegex: body.isRegex, + })).data; +} + +export async function deleteEmailFilter(body: GroupEmailFilterDelete_RequestDTO) { + return (await api.post("/group/settings/email-filter/delete", { + groupId: body.groupId, + groupPath: body.groupPath, + groupEmailFilterId: body.emailFilterId, + })).data; +} + +export async function listEmailFilter(body: GroupIdOrPath_RequestDTO) { + return (await api.post("/group/settings/email-filter/list", { + groupId: body.groupId, + groupPath: body.groupPath, + })).data; +} diff --git a/src/types/Group.ts b/src/types/Group.ts index 6ce43a97..b04b344b 100644 --- a/src/types/Group.ts +++ b/src/types/Group.ts @@ -19,6 +19,14 @@ export type Group = { organization: Group | null; }; +export type GroupEmailFilter = { + id: string; + enabled: boolean; + regex: boolean; + email: string; + added: string; +}; + export type GroupType = "INSTITUTION" | "CAMPUS" | "COURSE" | "PROJECT" | "CLASSROOM" | "MONITORIA" | "LABORATORY" | "ACADEMIC_CENTER" | "DEPARTMENT" | "STUDY_GROUP"; From b74ebb4d8273dc56e111482352047d8fc395ee9f Mon Sep 17 00:00:00 2001 From: Julio Verne <55258516+julio-ufpb@users.noreply.github.com> Date: Tue, 21 Nov 2023 09:04:00 -0300 Subject: [PATCH 03/20] =?UTF-8?q?change:=20autentica=C3=A7=C3=A3o=20=20ao?= =?UTF-8?q?=20editar=20perfil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ManageProfile/ManageProfile.tsx | 27 +++++++++++++++++------ src/services/UniversimeApi/Profile.ts | 1 + 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/pages/ManageProfile/ManageProfile.tsx b/src/pages/ManageProfile/ManageProfile.tsx index 2d5c6ab8..b059d938 100644 --- a/src/pages/ManageProfile/ManageProfile.tsx +++ b/src/pages/ManageProfile/ManageProfile.tsx @@ -110,6 +110,24 @@ export function ManageProfilePage() { } } + const { value: password, isConfirmed } = await SwalUtils.fireModal({ + title: "Edição de perfil", + input: "password", + inputLabel: "Inserir senha para salvar as alterações", + inputPlaceholder: "Insira sua senha", + confirmButtonText: "Confirmar Alterações", + showCancelButton: true, + cancelButtonText: "Cancelar", + allowOutsideClick: true, + showCloseButton: true, + inputAttributes: { + autocapitalize: "off", + autocorrect: "off" + } + }); + if (!isConfirmed) + return; + UniversimeApi.Profile.edit({ profileId: profile.id, name: firstname, @@ -117,19 +135,14 @@ export function ManageProfilePage() { bio, gender: gender || undefined, imageUrl: newImageUrl, + rawPassword: password, }).then(async res => { if (!res.success) throw new Error(res.message); const p = await authContext.updateLoggedUser(); navigate(`/profile/${p!.user.name}`); - }).catch((reason: Error) => { - SwalUtils.fireModal({ - title: "Erro ao salvar alterações de perfil", - text: reason.message, - icon: 'error', - }); - }) + }) } function submitLinkChanges(e: MouseEvent) { diff --git a/src/services/UniversimeApi/Profile.ts b/src/services/UniversimeApi/Profile.ts index 216e8289..f8060ddf 100644 --- a/src/services/UniversimeApi/Profile.ts +++ b/src/services/UniversimeApi/Profile.ts @@ -14,6 +14,7 @@ export type ProfileEdit_RequestDTO = { bio?: string; gender?: string; imageUrl?: string; + rawPassword?: string; }; export type ProfileIdAndUsername_RequestDTO = { From c89eeff02ff8c3d4a3bd93d33a15b85f760c00ae Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Wed, 22 Nov 2023 22:25:24 -0300 Subject: [PATCH 04/20] feat: email filter settings page --- .../GroupEmailFilter.less | 210 +++++++++++++++++ .../GroupEmailFilterLoader.ts | 28 +++ .../GroupEmailFilterPage.tsx | 220 ++++++++++++++++++ .../Settings/GroupEmailFilterPage/index.ts | 3 + src/pages/Settings/Settings.less | 3 +- src/pages/Settings/Settings.tsx | 2 +- .../SettingsDescription.tsx | 7 + src/pages/Settings/index.ts | 2 + src/services/routes.tsx | 9 +- 9 files changed, 481 insertions(+), 3 deletions(-) create mode 100644 src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less create mode 100644 src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterLoader.ts create mode 100644 src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx create mode 100644 src/pages/Settings/GroupEmailFilterPage/index.ts create mode 100644 src/pages/Settings/SettingsDescription/SettingsDescription.tsx diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less new file mode 100644 index 00000000..6c80a38f --- /dev/null +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less @@ -0,0 +1,210 @@ +@import url(/src/layouts/colors.less); +@import url(/src/layouts/fonts.less); +@import url(/src/layouts/transitions.less); +@import url(/src/layouts/border-radius.less); + +#email-filter-settings { + .create-new-filter { + width: fit-content; + margin-top: .5rem; + } + + .email-filter-list { + margin-top: 1rem; + + .email-filter-item { + &:not(:last-of-type) { + @margin-size: .75rem; + + padding-bottom: @margin-size; + margin-bottom: @margin-size; + border-bottom: solid 1px @card-item-color; + } + + .enabled-delete-wrapper { + display: flex; + flex-direction: row; + align-items: end; + justify-content: space-between; + + margin-bottom: .5rem; + + .enabled-wrapper { + display: flex; + flex-direction: row; + align-items: center; + + .filter-enabled-root { + @radix-switch-width: 2.5rem; + @radix-switch-height: 1.5rem; + @radix-thumb-diameter: @radix-switch-height; + + width: @radix-switch-width; + height: @radix-switch-height; + margin-right: .5rem; + + border-radius: 9999px; + border: none; + outline: 2px solid @primary-color; + + box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.75); + + cursor: pointer; + + &:focus { + outline: 2px solid @secondary-color; + } + + &:not([data-state='checked']) { + background-color: @card-background-color; + } + + &[data-state='checked'] { + background-color: @card-item-color; + + .filter-enabled-thumb { + transform: translateX(calc(@radix-switch-width - @radix-thumb-diameter)); + background-color: @primary-color; + } + } + + .filter-enabled-thumb { + display: block; + width: @radix-thumb-diameter; + height: @radix-thumb-diameter; + background-color: #FFF; + border-radius: 50%; + outline: 2px solid @primary-color; + + transition: transform 100ms; + will-change: transform; + } + } + } + + .delete-button { + background: transparent; + border: none; + + color: @font-color-v2; + font-size: 1.5rem; + + cursor: pointer; + will-change: color; + transition: @hover-transition-duration color; + + &:hover { + color: @wrong-invalid-color; + } + } + } + + .filter-type-email-wrapper { + @email-font-size: 1rem; + @email-padding: .5rem; + @trigger-width: 9.25em; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + .filter-type-trigger { + background-color: @card-item-color; + + border: none; + border-radius: .625rem; + padding: @email-padding; + width: @trigger-width; + + font-size: @email-font-size; + + cursor: pointer; + + .bi { + margin-left: .5em; + } + + &[data-state="open"] { + .bi::before { + content: "\F286"; + } + } + + &[data-state="closed"] { + .bi::before { + content: "\F282"; + } + } + } + + .filter-type-dropdown-menu { + background-color: @card-item-color; + border: solid 1px @card-background-color; + + border-radius: .625rem; + + overflow: hidden; + + .dropdown-options-item { + padding: .5em 1rem; + font-size: @email-font-size; + + cursor: pointer; + + &:hover { + outline: none; + background-color: @card-background-color; + } + } + } + + .filter-email-input { + font-size: @email-font-size; + padding: @email-padding; + + background-color: @card-item-color; + border: none; + border-radius: .625rem; + + width: calc(100% - @trigger-width - 1em); + } + } + } + } + + .buttons-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; + + margin-top: 1rem; + width: 100%; + + > button { + padding: .5em 1em; + border: none; + + font-size: 1rem; + text-transform: uppercase; + font-weight: @font-weight-bold; + + cursor: pointer; + + & + button { + margin-left: 1rem; + } + + &.submit { + color: @font-color-v1; + background-color: @primary-color; + border-radius: .625rem; + + &[disabled] { + background-color: @card-item-color; + } + } + } + } +} \ No newline at end of file diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterLoader.ts b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterLoader.ts new file mode 100644 index 00000000..c177181b --- /dev/null +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterLoader.ts @@ -0,0 +1,28 @@ +import { type LoaderFunctionArgs } from "react-router-dom"; +import { type GroupEmailFilter } from "@/types/Group"; +import UniversimeApi from "@/services/UniversimeApi"; + +export type GroupEmailFilterLoaderResponse = { + emailFilters: GroupEmailFilter[] | undefined; +}; + +export async function GroupEmailFilterFetch(organizationId: string): Promise { + const filters = await UniversimeApi.Group.listEmailFilter({ groupId: organizationId }); + + return { + emailFilters: filters.body?.emailFilters, + } +} + +export async function GroupEmailFilterLoader(args: LoaderFunctionArgs) { + const org = await UniversimeApi.User.organization(); + if (!org.success || !org.body?.organization) { + return FAILED_TO_LOAD; + } + + return GroupEmailFilterFetch(org.body.organization.id); +} + +const FAILED_TO_LOAD = { + emailFilters: undefined, +}; diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx new file mode 100644 index 00000000..14604afb --- /dev/null +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx @@ -0,0 +1,220 @@ +import { useReducer, useContext } from "react"; +import { useLoaderData, useNavigate } from "react-router-dom"; +import * as Switch from "@radix-ui/react-switch" +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +import { AuthContext } from "@/contexts/Auth"; +import { SettingsTitle, SettingsDescription } from "@/pages/Settings"; +import { ActionButton } from "@/components/ActionButton/ActionButton"; +import UniversimeApi from "@/services/UniversimeApi"; +import { type OptionInMenu, renderOption } from "@/utils/dropdownMenuUtils"; +import * as SwalUtils from "@/utils/sweetalertUtils"; + +import { type GroupEmailFilterLoaderResponse, GroupEmailFilterFetch } from "./GroupEmailFilterLoader"; +import { type GroupEmailFilter } from "@/types/Group"; +import "./GroupEmailFilter.less"; + +let NEW_FILTER_ID = 0; + +export function GroupEmailFilterPage() { + const auth = useContext(AuthContext); + const data = useLoaderData() as GroupEmailFilterLoaderResponse; + const navigate = useNavigate(); + const [emailFilters, emailFiltersDispatch] = useReducer(emailFilterReducer, data.emailFilters); + + if (emailFilters === undefined) { + SwalUtils.fireModal({ + title: "Erro ao recuperar filtros de email", + text: "Não foi possível recuperar os filtros de email", + + showCancelButton: false, + showConfirmButton: true, + confirmButtonText: "Voltar", + }).then(v => navigate("/settings")); + + return null; + } + + const currentEmailFilters = emailFilters.filter(f => f.state !== "DELETED"); + const canSave = emailFilters.filter(f => f.state !== undefined); + + const FILTER_TYPE_OPTIONS: OptionInMenu[] = [{ + text: "Terminado em", + onSelect(data) { + emailFiltersDispatch({ type: "EDIT", filter: {...data, regex: false} }); + }, + }, + { + text: "Padrão RegEx", + onSelect(data) { + emailFiltersDispatch({ type: "EDIT", filter: {...data, regex: true} }); + }, + }] + + + return
+ Filtros de email + Aqui você pode configurar quais emails são permitidos para cadastrar na plataforma. + + + +
+ { currentEmailFilters.length === 0 ?

Nenhum filtro para excluir.

: currentEmailFilters.map(filter => { + return
+
+
+ + + + { filter.enabled ? "Ativado" : "Desativado" } +
+ + +
+ +
+ + + + + + + { FILTER_TYPE_OPTIONS.map(def => renderOption(filter, def)) } + + + + emailFiltersDispatch({type: "EDIT", filter: {...filter, email: e.currentTarget.value}})} /> +
+
+ })} +
+ +
+ +
+
+ + function emailFilterReducer(state: EmailFilterOnList[] | undefined, action: EmailFilterReducerAction): EmailFilterOnList[] | undefined { + if (state === undefined) + return undefined; + + if (action.type === "CREATE") { + return [...state, { added: "", email: "@exemplo.com", enabled: true, id: (--NEW_FILTER_ID).toString(), regex: false, state: "NEW" }]; + } + + if (action.type === "DELETE") { + const filter = state.find(f => f.id === action.id)!; + + if (filter.state === "NEW") { + return state.filter(f => f.id !== action.id); + } + + else { + return state.map(f => f.id === action.id ? {...f, state: "DELETED"} : f); + } + } + + if (action.type === "EDIT") { + const newFilter: EmailFilterOnList = { + ...action.filter, + state: action.filter.state !== "NEW" ? "EDITED" : "NEW", + } + + return state.map(f => f.id === action.filter.id ? newFilter : f); + } + + if (action.type === "SET") { + return action.value; + } + } + + function makeToggleFilter(filter: EmailFilterOnList) { + return function(checked: boolean) { + emailFiltersDispatch({ + type: "EDIT", + filter: {...filter, enabled: checked}, + }); + } + } + + function makeDeleteFilter(filter: EmailFilterOnList) { + return function() { + SwalUtils.fireModal({ + title: "Deseja deletar esse filtro?", + text: "Essa ação não pode ser desfeita.", + + showCancelButton: true, + cancelButtonText: "Cancelar", + confirmButtonText: "Deletar", + confirmButtonColor: "var(--wrong-invalid-color)" + }).then(response => { + if (response.isConfirmed) { + emailFiltersDispatch({ + type: "DELETE", + id: filter.id, + }); + } + }); + } + } + + async function submitChanges() { + const toCreateFilters = emailFilters!.filter(f => f.state === "NEW"); + const toEditFilters = emailFilters!.filter(f => f.state === "EDITED"); + const toDeleteResponses = emailFilters!.filter(f => f.state === "DELETED"); + + Promise.all([ + Promise.all(toCreateFilters.map(f => UniversimeApi.Group.addEmailFilter({ email: f.email, groupId: auth.organization!.id, isEnabled: f.enabled, isRegex: f.regex }))), + Promise.all(toEditFilters.map(f => UniversimeApi.Group.editEmailFilter({ emailFilterId: f.id, groupId: auth.organization!.id, email: f.email, isEnabled: f.enabled, isRegex: f.regex }))), + Promise.all(toDeleteResponses.map(f => UniversimeApi.Group.deleteEmailFilter({ emailFilterId: f.id, groupId: auth.organization!.id }))), + ]).then(([createRes, editRes, deleteRes]) => { + const failedCreate = createRes.filter(f => !f.success); + const failedEdit = editRes.filter(f => !f.success); + const failedDelete = deleteRes.filter(f => !f.success); + + if (failedCreate.length + failedEdit.length + failedDelete.length === 0) { + return; + } + + const errorBuilder: string[] = []; + failedCreate.forEach(c => { + errorBuilder.push(`Ao criar filtro: ${c.message}`); + }); + failedEdit.forEach(c => { + errorBuilder.push(`Ao editar filtro: ${c.message}`); + }); + failedDelete.forEach(c => { + errorBuilder.push(`Ao deletar filtro: ${c.message}`); + }); + + refreshPage(); + + SwalUtils.fireModal({ + title: "Erro ao salvar filtros", + html: errorBuilder.join("
"), + icon: "error", + }); + }) + } + + async function refreshPage() { + const newData = await GroupEmailFilterFetch(auth.organization!.id); + + emailFiltersDispatch({ + type: "SET", + value: newData.emailFilters, + }); + } +} + +type EmailFilterOnList = GroupEmailFilter & { state?: "NEW" | "EDITED" | "DELETED"; }; + +type EmailFilterReducerAction = { type: "CREATE"; } | { type: "DELETE"; id: string; } | { type: "EDIT"; filter: EmailFilterOnList; } | { type: "SET", value: EmailFilterOnList[] | undefined }; diff --git a/src/pages/Settings/GroupEmailFilterPage/index.ts b/src/pages/Settings/GroupEmailFilterPage/index.ts new file mode 100644 index 00000000..0a59e69f --- /dev/null +++ b/src/pages/Settings/GroupEmailFilterPage/index.ts @@ -0,0 +1,3 @@ +export { GroupEmailFilterPage as default } from "./GroupEmailFilterPage"; +export * from "./GroupEmailFilterPage"; +export * from "./GroupEmailFilterLoader"; diff --git a/src/pages/Settings/Settings.less b/src/pages/Settings/Settings.less index 2330767e..229f6f2a 100644 --- a/src/pages/Settings/Settings.less +++ b/src/pages/Settings/Settings.less @@ -15,6 +15,7 @@ max-width: min(75%, 1024px); margin: @top-page-margin @left-page-padding; + padding: 1rem 1rem; border-radius: @border-radius; background-color: @card-background-color; @@ -29,7 +30,7 @@ .settings-page-title { font-size: 1.5rem; font-weight: @font-weight-bold; - padding: 1rem (@option-padding-x / 2) .25rem; + margin-bottom: .25rem; a { color: inherit; diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx index 3ddb0b58..dd3d7da7 100644 --- a/src/pages/Settings/Settings.tsx +++ b/src/pages/Settings/Settings.tsx @@ -20,7 +20,7 @@ export function SettingsPage() { Configurações - + } diff --git a/src/pages/Settings/SettingsDescription/SettingsDescription.tsx b/src/pages/Settings/SettingsDescription/SettingsDescription.tsx new file mode 100644 index 00000000..64df19b0 --- /dev/null +++ b/src/pages/Settings/SettingsDescription/SettingsDescription.tsx @@ -0,0 +1,7 @@ +import { PropsWithChildren } from "react"; + +export type SettingsDescriptionProps = PropsWithChildren<{}>; + +export function SettingsDescription(props: SettingsDescriptionProps) { + return

{ props.children }

+} diff --git a/src/pages/Settings/index.ts b/src/pages/Settings/index.ts index 1440070c..09fbf6bb 100644 --- a/src/pages/Settings/index.ts +++ b/src/pages/Settings/index.ts @@ -2,4 +2,6 @@ export { SettingsPage as default } from "./Settings"; export * from "./Settings"; export * from "./SettingsTitle/SettingsTitle"; +export * from "./SettingsDescription/SettingsDescription"; export * from "./SettingsOptions/SettingsMoveTo"; +export * from "./GroupEmailFilterPage"; diff --git a/src/services/routes.tsx b/src/services/routes.tsx index b46f486c..34771d9d 100644 --- a/src/services/routes.tsx +++ b/src/services/routes.tsx @@ -20,7 +20,7 @@ import Recovery from "@/pages/Recovery/Recovery"; import NewPassword from "@/pages/NewPassword/NewPassword"; import ManageProfilePage, { ManageProfileLoader } from "@/pages/ManageProfile"; import Homepage from "@/pages/Homepage"; -import SettingsPage from "@/pages/Settings"; +import SettingsPage, { GroupEmailFilterPage, GroupEmailFilterLoader } from "@/pages/Settings"; @@ -100,6 +100,13 @@ export const router = createBrowserRouter([{ { path: "/settings", element: , + children: [ + { + path: "email-filter", + element: , + loader: GroupEmailFilterLoader, + } + ], } ] }, From f16bc7d11001edc8565cec1d486b83604e7467d8 Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Wed, 22 Nov 2023 22:33:31 -0300 Subject: [PATCH 05/20] fix: refresh filters on save changes successfully --- src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx index 14604afb..28a7eca3 100644 --- a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx @@ -181,6 +181,7 @@ export function GroupEmailFilterPage() { const failedDelete = deleteRes.filter(f => !f.success); if (failedCreate.length + failedEdit.length + failedDelete.length === 0) { + refreshPage(); return; } From 7c6f92efba85f1d4066e94bf154814b99d994357 Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Wed, 22 Nov 2023 22:37:32 -0300 Subject: [PATCH 06/20] fix: canSave check --- .../Settings/GroupEmailFilterPage/GroupEmailFilter.less | 1 + .../Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less index 6c80a38f..a96f7f29 100644 --- a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less @@ -203,6 +203,7 @@ &[disabled] { background-color: @card-item-color; + color: @font-color-v2; } } } diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx index 28a7eca3..b27ad185 100644 --- a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilterPage.tsx @@ -36,7 +36,7 @@ export function GroupEmailFilterPage() { } const currentEmailFilters = emailFilters.filter(f => f.state !== "DELETED"); - const canSave = emailFilters.filter(f => f.state !== undefined); + const canSave = undefined !== emailFilters.find(f => f.state !== undefined); const FILTER_TYPE_OPTIONS: OptionInMenu[] = [{ text: "Terminado em", @@ -50,7 +50,7 @@ export function GroupEmailFilterPage() { emailFiltersDispatch({ type: "EDIT", filter: {...data, regex: true} }); }, }] - + return
Filtros de email @@ -98,7 +98,7 @@ export function GroupEmailFilterPage() {
- +
From d6c01b22db9236eb85503c6c511d960aa2cfa0a3 Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Fri, 24 Nov 2023 08:14:12 -0300 Subject: [PATCH 07/20] feat: roles page --- src/pages/Settings/RolesPage/RolesLoader.ts | 49 +++++++ src/pages/Settings/RolesPage/RolesPage.less | 0 src/pages/Settings/RolesPage/RolesPage.tsx | 155 ++++++++++++++++++++ src/pages/Settings/RolesPage/index.ts | 4 + src/pages/Settings/Settings.tsx | 2 +- src/pages/Settings/index.ts | 1 + src/services/UniversimeApi/Admin.ts | 44 ++++++ src/services/UniversimeApi/index.ts | 2 + src/services/routes.tsx | 9 +- src/types/User.ts | 6 + src/types/utils.ts | 4 +- src/utils/profileUtils.ts | 6 +- 12 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 src/pages/Settings/RolesPage/RolesLoader.ts create mode 100644 src/pages/Settings/RolesPage/RolesPage.less create mode 100644 src/pages/Settings/RolesPage/RolesPage.tsx create mode 100644 src/pages/Settings/RolesPage/index.ts create mode 100644 src/services/UniversimeApi/Admin.ts diff --git a/src/pages/Settings/RolesPage/RolesLoader.ts b/src/pages/Settings/RolesPage/RolesLoader.ts new file mode 100644 index 00000000..26c473d9 --- /dev/null +++ b/src/pages/Settings/RolesPage/RolesLoader.ts @@ -0,0 +1,49 @@ +import UniversimeApi from "@/services/UniversimeApi"; +import { LoaderFunctionArgs } from "react-router-dom"; +import { type Profile } from "@/types/Profile"; +import { type Optional } from "@/types/utils"; + +export type RolesPageLoaderResponse = { + success: true, + participants: Profile[]; +} | { + success: false, + reason: Optional, +}; + +export async function RolesPageFetch(organizationId: string): Promise { + const participants = await UniversimeApi.Group.participants({ groupId: organizationId }); + + if (!participants.success || !participants.body) { + return { + success: false, + reason: participants.message, + }; + } + + // if doesn't have access to accessLevel = not an admin + if (participants.body.participants.find(p => p.user.accessLevel === undefined)) { + return { + success: false, + reason: undefined, + }; + } + + return { + success: true, + participants: participants.body.participants, + }; +} + +export async function RolesPageLoader(args: LoaderFunctionArgs): Promise { + const organization = await UniversimeApi.User.organization(); + + if (!organization.success || !organization.body?.organization) { + return { + success: false, + reason: organization.message, + }; + } + + return RolesPageFetch(organization.body.organization.id); +} diff --git a/src/pages/Settings/RolesPage/RolesPage.less b/src/pages/Settings/RolesPage/RolesPage.less new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/Settings/RolesPage/RolesPage.tsx b/src/pages/Settings/RolesPage/RolesPage.tsx new file mode 100644 index 00000000..d81c9985 --- /dev/null +++ b/src/pages/Settings/RolesPage/RolesPage.tsx @@ -0,0 +1,155 @@ +import { useReducer, useState, useContext } from "react"; +import { useLoaderData, useNavigate } from "react-router-dom"; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +import UniversimeApi from "@/services/UniversimeApi"; +import { AuthContext } from "@/contexts/Auth"; +import { SettingsTitle, type RolesPageLoaderResponse, RolesPageFetch } from "@/pages/Settings"; +import { ProfileImage } from "@/components/ProfileImage/ProfileImage"; +import { setStateAsValue } from "@/utils/tsxUtils"; +import { getFullName, getProfileImageUrl } from "@/utils/profileUtils"; +import { type OptionInMenu, renderOption } from "@/utils/dropdownMenuUtils"; +import * as SwalUtils from "@/utils/sweetalertUtils"; + +import { type Profile } from "@/types/Profile"; +import { UserAccessLevelLabel, type UserAccessLevel } from "@/types/User"; +import { type Optional } from "@/types/utils"; +import "./RolesPage.less"; + +export function RolesPage() { + const data = useLoaderData() as RolesPageLoaderResponse; + const navigate = useNavigate(); + const auth = useContext(AuthContext); + + const [participants, participantsDispatch] = useReducer(participantsReducer, data.success ? data.participants : undefined); + const [filter, setFilter] = useState(""); + + if (!participants) { + SwalUtils.fireModal({ + title: "Erro ao recuperar dados dos participantes", + text: data.success === false ? data.reason : undefined, + + showConfirmButton: true, + confirmButtonText: "Voltar", + }).then(res => navigate("/settings")); + return null; + } + + const filteredParticipants = participants.filter(p => p.firstname?.toLocaleLowerCase().includes(filter.toLocaleLowerCase())); + const CHANGE_ROLE_OPTIONS: OptionInMenu[] = Object.entries(UserAccessLevelLabel).map(([role, label]) => ({ + text: label, + onSelect(data) { + participantsDispatch({ + type: "SET_ROLE", + profileId: data.id, + setRole: role as UserAccessLevel, + }); + }, + })); + + const canSubmit = participants.filter(p => p.changed).length > 0; + + return
+ Configurar administradores + {/* todo: put description */} +
+ + +
+ +
+ { filteredParticipants.map(profile => { + return
+ +
+

{getFullName(profile)}

+

{profile.bio}

+
+ + + + + + + { CHANGE_ROLE_OPTIONS.map(def => renderOption(profile, def)) } + + +
+ }) } +
+
+ + function participantsReducer(state: Optional, action: ParticipantsReducerAction): Optional { + if (state === undefined || data.success === false) + return undefined; + + if (action.type === "SET_ALL") { + return action.setParticipants; + } + + return state.map(p => { + if (p.id !== action.profileId) + return p; + + const unchanging = data.participants + .find(p => p.id === action.profileId)! + .user.accessLevel === action.setRole; + + return { + ...p, + changed: unchanging ? undefined : true, + user: { + ...p.user, + accessLevel: action.setRole, + } + } + }); + } + + async function refreshPage() { + const response = await RolesPageFetch(auth.organization!.id); + participantsDispatch({ + type: "SET_ALL", + setParticipants: response.success ? response.participants : undefined, + }); + } + + async function submitChanges() { + if (!participants) + return; + + const toUpdate = participants.filter(p => p.changed); + const responses = await Promise.all( + toUpdate.map(p => UniversimeApi.Admin.editAccount({ userId: p.user.id, authorityLevel: p.user.accessLevel })) + ); + + refreshPage(); + + const failedChanges = responses.filter(r => !r.success); + if (failedChanges.length === 0) + return; + + const messages = failedChanges + .map(err => err.message) + .filter(m => m !== undefined); + + SwalUtils.fireModal({ + title: "Erro ao alterar administradores", + text: messages.join("\n"), + }); + } +} + +type ProfileOnList = Profile & { changed?: true }; + +type ParticipantsReducerAction = { + type: "SET_ROLE"; + setRole: UserAccessLevel; + profileId: string; +} | { + type: "SET_ALL"; + setParticipants: Optional; +}; diff --git a/src/pages/Settings/RolesPage/index.ts b/src/pages/Settings/RolesPage/index.ts new file mode 100644 index 00000000..1d4afbc8 --- /dev/null +++ b/src/pages/Settings/RolesPage/index.ts @@ -0,0 +1,4 @@ +export { RolesPage as default } from "./RolesPage"; + +export * from "./RolesPage"; +export * from "./RolesLoader"; diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx index 3ddb0b58..f8e3b466 100644 --- a/src/pages/Settings/Settings.tsx +++ b/src/pages/Settings/Settings.tsx @@ -19,7 +19,7 @@ export function SettingsPage() { { outlet ?? <> Configurações - + diff --git a/src/pages/Settings/index.ts b/src/pages/Settings/index.ts index 1440070c..c9eb1e61 100644 --- a/src/pages/Settings/index.ts +++ b/src/pages/Settings/index.ts @@ -3,3 +3,4 @@ export { SettingsPage as default } from "./Settings"; export * from "./Settings"; export * from "./SettingsTitle/SettingsTitle"; export * from "./SettingsOptions/SettingsMoveTo"; +export * from "./RolesPage"; diff --git a/src/services/UniversimeApi/Admin.ts b/src/services/UniversimeApi/Admin.ts new file mode 100644 index 00000000..b57fcb72 --- /dev/null +++ b/src/services/UniversimeApi/Admin.ts @@ -0,0 +1,44 @@ +import { api } from "./api"; +import { type ApiResponse } from "@/types/UniversimeApi"; +import { type User, type UserAccessLevel } from "@/types/User"; + +export type AdminEditAccount_RequestDTO = { + userId: string; + username?: string; + email?: string; + password?: string; + authorityLevel?: UserAccessLevel; + emailVerified?: boolean; + blockedAccount?: boolean; + inactiveAccount?: boolean; + credentialsExpired?: boolean; + expiredUser?: boolean; +}; + +export type AdminListAccounts_RequestDTO = { + accessLevel?: UserAccessLevel; +}; + +export type AdminEditAccount_ResponseDTO = ApiResponse; +export type AdminListAccounts_ResponseDTO = ApiResponse<{ users: User[] }>; + +export async function editAccount(body: AdminEditAccount_RequestDTO) { + return (await api.post("/admin/account/edit", { + userId: body.userId, + username: body.username, + email: body.email, + password: body.password, + authorityLevel: body.authorityLevel, + emailVerified: body.emailVerified, + blockedAccount: body.blockedAccount, + inactiveAccount: body.inactiveAccount, + credentialsExpired: body.credentialsExpired, + expiredUser: body.expiredUser, + })).data; +} + +export async function listAccounts(body: AdminListAccounts_RequestDTO) { + return (await api.post("/admin/account/list", { + accessLevel: body.accessLevel, + })).data; +} diff --git a/src/services/UniversimeApi/index.ts b/src/services/UniversimeApi/index.ts index f10d80a0..2092176b 100644 --- a/src/services/UniversimeApi/index.ts +++ b/src/services/UniversimeApi/index.ts @@ -8,6 +8,7 @@ import * as Profile from "./Profile"; import * as Capacity from "./Capacity" import * as User from "./User" import * as Image from "./Image" +import * as Admin from "./Admin" export const UniversimeApi = { api, @@ -20,6 +21,7 @@ export const UniversimeApi = { Profile, User, Image, + Admin, }; export default UniversimeApi; diff --git a/src/services/routes.tsx b/src/services/routes.tsx index b46f486c..54d44550 100644 --- a/src/services/routes.tsx +++ b/src/services/routes.tsx @@ -20,7 +20,7 @@ import Recovery from "@/pages/Recovery/Recovery"; import NewPassword from "@/pages/NewPassword/NewPassword"; import ManageProfilePage, { ManageProfileLoader } from "@/pages/ManageProfile"; import Homepage from "@/pages/Homepage"; -import SettingsPage from "@/pages/Settings"; +import SettingsPage, { RolesPage, RolesPageLoader } from "@/pages/Settings"; @@ -100,6 +100,13 @@ export const router = createBrowserRouter([{ { path: "/settings", element: , + children: [ + { + path: "roles", + element: , + loader: RolesPageLoader, + } + ], } ] }, diff --git a/src/types/User.ts b/src/types/User.ts index 0243ebda..0f7b9ab0 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -8,3 +8,9 @@ export type User = { needProfile: boolean; accessLevel?: UserAccessLevel; } + +export const UserAccessLevelLabel: { [k in UserAccessLevel]: string } = { + ROLE_ADMIN: "Administrador", + ROLE_DEV: "Desenvolvedor", + ROLE_USER: "Usuário", +} diff --git a/src/types/utils.ts b/src/types/utils.ts index 4aa98cda..32471f22 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -1 +1,3 @@ -export type NullableBoolean = null | boolean; +export type Nullable = null | T; +export type NullableBoolean = Nullable; +export type Optional = T | undefined; diff --git a/src/utils/profileUtils.ts b/src/utils/profileUtils.ts index 2879219d..6c792698 100644 --- a/src/utils/profileUtils.ts +++ b/src/utils/profileUtils.ts @@ -30,7 +30,11 @@ export function getGenderName(gender: Gender | null | undefined): string { } export function getProfileImageUrl(profile: Profile): string | null { - return profile.image && profile.image.startsWith("/") + if (!profile.image) { + return "/assets/imgs/default_avatar.png"; + } + + return profile.image.startsWith("/") ? `${import.meta.env.VITE_UNIVERSIME_API}${profile.image}` : profile.image; } From af3543503ed2472c22aef2603ff6840fb4771830 Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Fri, 24 Nov 2023 08:15:13 -0300 Subject: [PATCH 08/20] fix: typo --- src/pages/Settings/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx index f8e3b466..f8149d37 100644 --- a/src/pages/Settings/Settings.tsx +++ b/src/pages/Settings/Settings.tsx @@ -19,7 +19,7 @@ export function SettingsPage() { { outlet ?? <> Configurações - + From 221ff4a98614d5858d25ce1a62ad469ab5ad83e1 Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Fri, 24 Nov 2023 08:38:28 -0300 Subject: [PATCH 09/20] change: base roles page css --- src/pages/Settings/RolesPage/RolesPage.less | 131 ++++++++++++++++++++ src/pages/Settings/RolesPage/RolesPage.tsx | 4 +- 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/src/pages/Settings/RolesPage/RolesPage.less b/src/pages/Settings/RolesPage/RolesPage.less index e69de29b..cdb99ad3 100644 --- a/src/pages/Settings/RolesPage/RolesPage.less +++ b/src/pages/Settings/RolesPage/RolesPage.less @@ -0,0 +1,131 @@ +@import url(/src/layouts/colors.less); +@import url(/src/layouts/fonts.less); + +#roles-settings { + #search-submit-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + width: 100%; + margin-bottom: 1rem; + + font-size: 1rem; + + @submit-width: 11em; + @padding: .75em 0; + @search-width: calc(100% - 1em - @submit-width); + + input[type="search"] { + padding: @padding; + border-radius: .625rem; + border: none; + + width: @search-width; + + &:focus { + outline-color: @primary-color; + } + } + + .submit { + padding: @padding; + border-radius: .625rem; + border: none; + + width: @submit-width; + + background-color: @primary-color; + + color: @font-color-v1; + text-transform: uppercase; + font-weight: @font-weight-bold; + + cursor: pointer; + + &:disabled { + background-color: @card-item-color; + color: @font-color-v2; + cursor: default; + } + } + } + + #participants-list { + .profile-item { + display: flex; + flex-direction: row; + align-items: start; + + @image-width: 7.5rem; + @set-role-width: 9.5rem; + @role-font-size: 1rem; + + &:not(:last-child) { + margin-bottom: 1.5rem; + } + + .profile-image { + width: @image-width; + } + + .info { + margin: .5em .5em; + } + + .set-role-trigger { + width: @set-role-width; + font-size: @role-font-size; + + margin-left: auto; + + border: none; + border-radius: .625rem; + background-color: @card-item-color; + padding: .5em; + + cursor: pointer; + + .bi { + margin-left: .5em; + } + + &[data-state="open"] .bi::before { + content: "\F286"; + } + + &[data-state="closed"] .bi::before { + content: "\F282"; + } + } + + .set-role-menu { + width: @set-role-width; + font-size: @role-font-size; + + background-color: @card-item-color; + border-radius: .625rem; + border: solid 2px @card-background-color; + + display: flex; + flex-direction: column; + align-items: center; + + overflow: hidden; + + .dropdown-options-item { + width: @set-role-width; + text-align: center; + + padding: .75em 0; + cursor: pointer; + + &:hover{ + background-color: @card-background-color; + } + } + } + } + } +} diff --git a/src/pages/Settings/RolesPage/RolesPage.tsx b/src/pages/Settings/RolesPage/RolesPage.tsx index d81c9985..0d869cdc 100644 --- a/src/pages/Settings/RolesPage/RolesPage.tsx +++ b/src/pages/Settings/RolesPage/RolesPage.tsx @@ -53,8 +53,8 @@ export function RolesPage() { Configurar administradores {/* todo: put description */}
- - + +
From 576b86d49038e4480210d66be300e53b918598b4 Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Fri, 24 Nov 2023 08:45:54 -0300 Subject: [PATCH 10/20] change: adjust roles page css --- src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less | 1 - src/pages/Settings/RolesPage/RolesPage.less | 3 ++- src/pages/Settings/RolesPage/RolesPage.tsx | 4 ++-- src/pages/Settings/Settings.less | 4 ++++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less index a96f7f29..65b526e5 100644 --- a/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less +++ b/src/pages/Settings/GroupEmailFilterPage/GroupEmailFilter.less @@ -6,7 +6,6 @@ #email-filter-settings { .create-new-filter { width: fit-content; - margin-top: .5rem; } .email-filter-list { diff --git a/src/pages/Settings/RolesPage/RolesPage.less b/src/pages/Settings/RolesPage/RolesPage.less index cdb99ad3..69bf7265 100644 --- a/src/pages/Settings/RolesPage/RolesPage.less +++ b/src/pages/Settings/RolesPage/RolesPage.less @@ -14,7 +14,7 @@ font-size: 1rem; @submit-width: 11em; - @padding: .75em 0; + @padding: .75em .25em; @search-width: calc(100% - 1em - @submit-width); input[type="search"] { @@ -78,6 +78,7 @@ width: @set-role-width; font-size: @role-font-size; + margin-top: .5em; margin-left: auto; border: none; diff --git a/src/pages/Settings/RolesPage/RolesPage.tsx b/src/pages/Settings/RolesPage/RolesPage.tsx index 0d869cdc..61b7140e 100644 --- a/src/pages/Settings/RolesPage/RolesPage.tsx +++ b/src/pages/Settings/RolesPage/RolesPage.tsx @@ -4,7 +4,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import UniversimeApi from "@/services/UniversimeApi"; import { AuthContext } from "@/contexts/Auth"; -import { SettingsTitle, type RolesPageLoaderResponse, RolesPageFetch } from "@/pages/Settings"; +import { SettingsTitle, type RolesPageLoaderResponse, RolesPageFetch, SettingsDescription } from "@/pages/Settings"; import { ProfileImage } from "@/components/ProfileImage/ProfileImage"; import { setStateAsValue } from "@/utils/tsxUtils"; import { getFullName, getProfileImageUrl } from "@/utils/profileUtils"; @@ -51,7 +51,7 @@ export function RolesPage() { return
Configurar administradores - {/* todo: put description */} + Configure os níveis de acesso dos usuários do Universi.me
diff --git a/src/pages/Settings/Settings.less b/src/pages/Settings/Settings.less index 229f6f2a..c5d44ff4 100644 --- a/src/pages/Settings/Settings.less +++ b/src/pages/Settings/Settings.less @@ -38,6 +38,10 @@ } } + .settings-description { + margin-bottom: .5rem; + } + > .settings-option { display: flex; flex-direction: column; From bb831d5fc2a31c14164b0d4f76e9ea94e5a2cf62 Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Fri, 24 Nov 2023 08:53:51 -0300 Subject: [PATCH 11/20] change: sort users --- src/pages/Settings/RolesPage/RolesPage.tsx | 13 ++++++++-- src/types/User.ts | 28 ++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/pages/Settings/RolesPage/RolesPage.tsx b/src/pages/Settings/RolesPage/RolesPage.tsx index 61b7140e..ee258a9e 100644 --- a/src/pages/Settings/RolesPage/RolesPage.tsx +++ b/src/pages/Settings/RolesPage/RolesPage.tsx @@ -12,7 +12,7 @@ import { type OptionInMenu, renderOption } from "@/utils/dropdownMenuUtils"; import * as SwalUtils from "@/utils/sweetalertUtils"; import { type Profile } from "@/types/Profile"; -import { UserAccessLevelLabel, type UserAccessLevel } from "@/types/User"; +import { UserAccessLevelLabel, type UserAccessLevel, compareAccessLevel } from "@/types/User"; import { type Optional } from "@/types/utils"; import "./RolesPage.less"; @@ -35,7 +35,16 @@ export function RolesPage() { return null; } - const filteredParticipants = participants.filter(p => p.firstname?.toLocaleLowerCase().includes(filter.toLocaleLowerCase())); + const filteredParticipants = participants + .filter(p => p.firstname?.toLocaleLowerCase().includes(filter.toLocaleLowerCase())) + .toSorted((a, b) => { + if (a.user.accessLevel !== b.user.accessLevel) { + return compareAccessLevel(a.user.accessLevel!, b.user.accessLevel!); + } + + return getFullName(a).localeCompare(getFullName(b)); + }); + const CHANGE_ROLE_OPTIONS: OptionInMenu[] = Object.entries(UserAccessLevelLabel).map(([role, label]) => ({ text: label, onSelect(data) { diff --git a/src/types/User.ts b/src/types/User.ts index 0f7b9ab0..9a2d4650 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -14,3 +14,31 @@ export const UserAccessLevelLabel: { [k in UserAccessLevel]: string } = { ROLE_DEV: "Desenvolvedor", ROLE_USER: "Usuário", } + +export function compareAccessLevel(a: UserAccessLevel, b: UserAccessLevel): number { + const A_FIRST = -1; + const KEEP_ORDER = 0; + const B_FIRST = 1; + + if (a === b) { + return KEEP_ORDER; + } + + if (a === "ROLE_ADMIN") { + return A_FIRST; + } + + if (b === "ROLE_ADMIN") { + return B_FIRST; + } + + if (a === "ROLE_DEV") { + return A_FIRST; + } + + if (b === "ROLE_DEV") { + return B_FIRST; + } + + return KEEP_ORDER; +} From 667fbec9ea4c79059dc2f55f75893fca522fd07e Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Fri, 24 Nov 2023 09:06:49 -0300 Subject: [PATCH 12/20] fix: change own level --- src/pages/Settings/RolesPage/RolesPage.less | 10 +++++++++- src/pages/Settings/RolesPage/RolesPage.tsx | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/pages/Settings/RolesPage/RolesPage.less b/src/pages/Settings/RolesPage/RolesPage.less index 69bf7265..d5af3640 100644 --- a/src/pages/Settings/RolesPage/RolesPage.less +++ b/src/pages/Settings/RolesPage/RolesPage.less @@ -71,7 +71,11 @@ } .info { - margin: .5em .5em; + @margin-y: .5rem; + margin-top: @margin-y; + margin-bottom: @margin-y; + margin-left: auto; + margin-right: 1rem; } .set-role-trigger { @@ -88,6 +92,10 @@ cursor: pointer; + &:disabled { + cursor: default; + } + .bi { margin-left: .5em; } diff --git a/src/pages/Settings/RolesPage/RolesPage.tsx b/src/pages/Settings/RolesPage/RolesPage.tsx index ee258a9e..26824780 100644 --- a/src/pages/Settings/RolesPage/RolesPage.tsx +++ b/src/pages/Settings/RolesPage/RolesPage.tsx @@ -68,6 +68,8 @@ export function RolesPage() {
{ filteredParticipants.map(profile => { + const isOwnProfile = auth.profile!.id === profile.id; + return
@@ -75,7 +77,7 @@ export function RolesPage() {

{profile.bio}

- + @@ -107,7 +101,7 @@ export function GroupEmailFilterPage() { return undefined; if (action.type === "CREATE") { - return [...state, { added: "", email: "@exemplo.com", enabled: true, id: (--NEW_FILTER_ID).toString(), regex: false, state: "NEW" }]; + return [...state, { added: "", email: "@exemplo.com", enabled: true, id: (--NEW_FILTER_ID).toString(), type: "END_WITH", state: "NEW" }]; } if (action.type === "DELETE") { @@ -172,8 +166,8 @@ export function GroupEmailFilterPage() { const toDeleteResponses = emailFilters!.filter(f => f.state === "DELETED"); Promise.all([ - Promise.all(toCreateFilters.map(f => UniversimeApi.Group.addEmailFilter({ email: f.email, groupId: auth.organization!.id, isEnabled: f.enabled, isRegex: f.regex }))), - Promise.all(toEditFilters.map(f => UniversimeApi.Group.editEmailFilter({ emailFilterId: f.id, groupId: auth.organization!.id, email: f.email, isEnabled: f.enabled, isRegex: f.regex }))), + Promise.all(toCreateFilters.map(f => UniversimeApi.Group.addEmailFilter({ email: f.email, groupId: auth.organization!.id, isEnabled: f.enabled, type: f.type }))), + Promise.all(toEditFilters.map(f => UniversimeApi.Group.editEmailFilter({ emailFilterId: f.id, groupId: auth.organization!.id, email: f.email, isEnabled: f.enabled, type: f.type }))), Promise.all(toDeleteResponses.map(f => UniversimeApi.Group.deleteEmailFilter({ emailFilterId: f.id, groupId: auth.organization!.id }))), ]).then(([createRes, editRes, deleteRes]) => { const failedCreate = createRes.filter(f => !f.success); diff --git a/src/services/UniversimeApi/Group.ts b/src/services/UniversimeApi/Group.ts index 6c5f4b92..1265ef4b 100644 --- a/src/services/UniversimeApi/Group.ts +++ b/src/services/UniversimeApi/Group.ts @@ -1,4 +1,4 @@ -import type { Group, GroupType, GroupEmailFilter } from "@/types/Group"; +import type { Group, GroupType, GroupEmailFilter, GroupEmailFilterType } from "@/types/Group"; import type { Profile } from "@/types/Profile"; import type { ApiResponse } from "@/types/UniversimeApi"; import { api } from "./api"; @@ -42,14 +42,14 @@ export type GroupIdOrPath_RequestDTO = { export type GroupEmailFilterAdd_RequestDTO = GroupIdOrPath_RequestDTO & { email: string; isEnabled?: boolean; - isRegex?: boolean; + type?: GroupEmailFilterType; }; export type GroupEmailFilterEdit_RequestDTO = GroupIdOrPath_RequestDTO & { emailFilterId: string; email?: string; isEnabled?: boolean; - isRegex?: boolean; + type?: GroupEmailFilterType; }; export type GroupEmailFilterDelete_RequestDTO = GroupIdOrPath_RequestDTO & { @@ -147,7 +147,7 @@ export async function addEmailFilter(body: GroupEmailFilterAdd_RequestDTO) { groupPath: body.groupPath, email: body.email, enabled: body.isEnabled, - isRegex: body.isRegex, + type: body.type, })).data; } @@ -158,7 +158,7 @@ export async function editEmailFilter(body: GroupEmailFilterEdit_RequestDTO) { groupEmailFilterId: body.emailFilterId, email: body.email, enabled: body.isEnabled, - isRegex: body.isRegex, + type: body.type, })).data; } diff --git a/src/types/Group.ts b/src/types/Group.ts index b04b344b..db585a84 100644 --- a/src/types/Group.ts +++ b/src/types/Group.ts @@ -22,11 +22,22 @@ export type Group = { export type GroupEmailFilter = { id: string; enabled: boolean; - regex: boolean; + type: GroupEmailFilterType; email: string; added: string; }; +export type GroupEmailFilterType = "END_WITH" | "START_WITH" | "CONTAINS" | "EQUALS" | "MASK" | "REGEX"; + +export const GroupEmailFilterTypeToLabel = { + "END_WITH": "Terminando em", + "START_WITH": "Começando com", + "CONTAINS": "Contendo", + "EQUALS": "Igual a", + "MASK": "Máscara ( * )", + "REGEX": "Padrão RegEx", +}; + export type GroupType = "INSTITUTION" | "CAMPUS" | "COURSE" | "PROJECT" | "CLASSROOM" | "MONITORIA" | "LABORATORY" | "ACADEMIC_CENTER" | "DEPARTMENT" | "STUDY_GROUP"; From 0e794ca3fe5d71cce434da277a03304e6258b6a2 Mon Sep 17 00:00:00 2001 From: Julio Verne <55258516+julio-ufpb@users.noreply.github.com> Date: Sat, 25 Nov 2023 20:40:28 -0300 Subject: [PATCH 16/20] change: firstname and lastname in signup --- src/pages/SignUp/SignUpModal/SignUpModal.less | 31 +++++++++++++++++++ src/pages/SignUp/SignUpModal/SignUpModal.tsx | 27 +++++++++++++++- src/services/UniversimeApi/User.ts | 4 +++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/pages/SignUp/SignUpModal/SignUpModal.less b/src/pages/SignUp/SignUpModal/SignUpModal.less index 73dd0549..8db2c07b 100644 --- a/src/pages/SignUp/SignUpModal/SignUpModal.less +++ b/src/pages/SignUp/SignUpModal/SignUpModal.less @@ -85,6 +85,37 @@ font-weight: @font-weight-semibold; } + &#fieldset-name { + display: flex; + flex-direction: column; + + + label { + display: flex; + flex-direction: column; + + &:not(:last-of-type) { + margin-bottom: 1rem; + } + + input { + width: 100%; + } + } + + .counter-wrapper { + display: flex; + width: 100%; + flex-direction: row; + justify-content: space-between; + align-items: end; + + .counter { + font-size: .75em; + } + } + } + &.invalid-email { input[name="email"] { background-color: #F5E0E0; diff --git a/src/pages/SignUp/SignUpModal/SignUpModal.tsx b/src/pages/SignUp/SignUpModal/SignUpModal.tsx index b5118693..11cbfb42 100644 --- a/src/pages/SignUp/SignUpModal/SignUpModal.tsx +++ b/src/pages/SignUp/SignUpModal/SignUpModal.tsx @@ -5,6 +5,7 @@ import ReCAPTCHA from "react-google-recaptcha-enterprise"; import { UniversiModal } from "@/components/UniversiModal"; import UniversimeApi from "@/services/UniversimeApi"; import { isEmail } from "@/utils/regexUtils"; +import { setStateAsValue } from "@/utils/tsxUtils"; import { minimumLength, numberOrSpecialChar, passwordValidationClass, upperAndLowerCase } from "@/utils/passwordValidation"; import { enableSignUp } from "./helperFunctions"; import * as SwalUtils from "@/utils/sweetalertUtils"; @@ -15,12 +16,16 @@ export type SignUpModalProps = { toggleModal: (state: boolean) => any; }; +const FIRST_NAME_MAX_LENGTH = 21; +const LAST_NAME_MAX_LENGTH = 21; const USERNAME_CHAR_REGEX = /[a-z0-9_.-]/ export function SignUpModal(props: SignUpModalProps) { const navigate = useNavigate(); + const [firstname, setFirstname] = useState(""); + const [lastname, setLastname] = useState(""); const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -36,6 +41,8 @@ export function SignUpModal(props: SignUpModalProps) { const [recaptchaToken, setRecaptchaToken] = useState(null); const [recaptchaRef, setRecaptchaRef] = useState(null); + const isFirstnameFull = (firstname.length) >= FIRST_NAME_MAX_LENGTH; + const isLastnameFull = (lastname.length) >= LAST_NAME_MAX_LENGTH; const canSignUp = enableSignUp(username, email, password); @@ -88,6 +95,24 @@ export function SignUpModal(props: SignUpModalProps) {
+
+ + + +
Email ) { e.preventDefault(); - UniversimeApi.User.signUp({ username, email, password, recaptchaToken }) + UniversimeApi.User.signUp({ firstname, lastname, username, email, password, recaptchaToken }) .then(res => { if (!res.success) { recaptchaRef.reset(); diff --git a/src/services/UniversimeApi/User.ts b/src/services/UniversimeApi/User.ts index f43afd18..33cbd6ae 100644 --- a/src/services/UniversimeApi/User.ts +++ b/src/services/UniversimeApi/User.ts @@ -3,6 +3,8 @@ import { api } from "./api"; import { Group } from "@/types/Group"; export type UserSignUp_RequestDTO = { + firstname: string; + lastname: string; username: string; email: string; password: string; @@ -38,6 +40,8 @@ export type UserOrganization_ResponseDTO = ApiResponse<{organization : Group | n export async function signUp(body: UserSignUp_RequestDTO) { return (await api.post("/signup", { + firstname: body.firstname, + lastname: body.lastname, username: body.username, email: body.email, password: body.password, From a4299a27bd206afc3f9556d8b0711bc35e2a6565 Mon Sep 17 00:00:00 2001 From: Julio Verne <55258516+julio-ufpb@users.noreply.github.com> Date: Sat, 25 Nov 2023 20:46:29 -0300 Subject: [PATCH 17/20] change: add placeholder in input field --- src/pages/SignUp/SignUpModal/SignUpModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/SignUp/SignUpModal/SignUpModal.tsx b/src/pages/SignUp/SignUpModal/SignUpModal.tsx index 11cbfb42..3e19f5e9 100644 --- a/src/pages/SignUp/SignUpModal/SignUpModal.tsx +++ b/src/pages/SignUp/SignUpModal/SignUpModal.tsx @@ -101,7 +101,7 @@ export function SignUpModal(props: SignUpModalProps) { Nome {firstname.length} / {FIRST_NAME_MAX_LENGTH} - +
From 111e1d0b213169664a0772c07c18c8ee90f5a4cb Mon Sep 17 00:00:00 2001 From: Douglas Sebastian Date: Mon, 27 Nov 2023 11:05:58 -0300 Subject: [PATCH 18/20] fix: admin can't edit content --- .../GroupContentMaterials/GroupContentMaterials.tsx | 8 +++----- .../GroupContents/GroupContents/GroupContents.tsx | 8 +++----- src/types/Group.ts | 1 + 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/pages/Group/GroupTabs/GroupContents/GroupContentMaterials/GroupContentMaterials.tsx b/src/pages/Group/GroupTabs/GroupContents/GroupContentMaterials/GroupContentMaterials.tsx index 50534aa0..9c9cb304 100644 --- a/src/pages/Group/GroupTabs/GroupContents/GroupContentMaterials/GroupContentMaterials.tsx +++ b/src/pages/Group/GroupTabs/GroupContents/GroupContentMaterials/GroupContentMaterials.tsx @@ -38,7 +38,7 @@ export function GroupContentMaterials() { groupContext.setEditMaterial(data); }, hidden() { - return groupContext?.group.admin.id !== groupContext?.loggedData.profile.id; + return !groupContext?.group.canEdit; }, }, { @@ -47,7 +47,7 @@ export function GroupContentMaterials() { className: "delete", onSelect: handleDeleteMaterial, hidden() { - return groupContext?.group.admin.id !== groupContext?.loggedData.profile.id; + return !groupContext?.group.canEdit; }, } ]; @@ -58,10 +58,8 @@ export function GroupContentMaterials() {
{ - groupContext.loggedData.profile.id == groupContext.group.admin.id || groupContext.loggedData.profile?.id == groupContext.group.organization?.admin.id ? + groupContext.group.canEdit && - : - <> }
diff --git a/src/pages/Group/GroupTabs/GroupContents/GroupContents/GroupContents.tsx b/src/pages/Group/GroupTabs/GroupContents/GroupContents/GroupContents.tsx index 3004b185..9cf32a6e 100644 --- a/src/pages/Group/GroupTabs/GroupContents/GroupContents/GroupContents.tsx +++ b/src/pages/Group/GroupTabs/GroupContents/GroupContents/GroupContents.tsx @@ -33,7 +33,7 @@ export function GroupContents() { groupContext.setEditContent(data); }, hidden() { - return groupContext?.group.admin.id !== groupContext?.loggedData.profile.id; + return !groupContext?.group.canEdit; }, }, { @@ -42,7 +42,7 @@ export function GroupContents() { className: "delete", onSelect: handleDeleteContent, hidden() { - return groupContext?.group.admin.id !== groupContext?.loggedData.profile.id; + return !groupContext?.group.canEdit; }, } ] @@ -53,12 +53,10 @@ export function GroupContents() {
{ - authContext.profile?.id == groupContext.group.admin.id || authContext.profile?.id == groupContext.group.organization?.admin.id ? + groupContext.group.canEdit && - : - <> }
diff --git a/src/types/Group.ts b/src/types/Group.ts index db585a84..3eb94ad5 100644 --- a/src/types/Group.ts +++ b/src/types/Group.ts @@ -17,6 +17,7 @@ export type Group = { rootGroup: boolean; bannerImage: string | null; organization: Group | null; + canEdit: boolean; }; export type GroupEmailFilter = { From 59743a02c0492edeb64c0df1ab3538bf5aded863 Mon Sep 17 00:00:00 2001 From: Julio Verne <55258516+julio-ufpb@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:00:24 -0300 Subject: [PATCH 19/20] feat: environment variables from settings --- src/contexts/Auth/AuthProvider.tsx | 3 +- src/pages/Recovery/Recovery.tsx | 9 +- .../EnvironmentsPage/EnvironmentsLoader.ts | 15 ++ .../EnvironmentsPage/EnvironmentsPage.less | 183 ++++++++++++++++++ .../EnvironmentsPage/EnvironmentsPage.tsx | 165 ++++++++++++++++ src/pages/Settings/EnvironmentsPage/index.ts | 4 + src/pages/Settings/Settings.tsx | 1 + src/pages/Settings/index.ts | 1 + src/pages/SignUp/SignUpModal/SignUpModal.tsx | 9 +- src/pages/singin/SinginForm.tsx | 9 +- src/services/UniversimeApi/Group.ts | 8 + src/services/routes.tsx | 7 +- 12 files changed, 402 insertions(+), 12 deletions(-) create mode 100644 src/pages/Settings/EnvironmentsPage/EnvironmentsLoader.ts create mode 100644 src/pages/Settings/EnvironmentsPage/EnvironmentsPage.less create mode 100644 src/pages/Settings/EnvironmentsPage/EnvironmentsPage.tsx create mode 100644 src/pages/Settings/EnvironmentsPage/index.ts diff --git a/src/contexts/Auth/AuthProvider.tsx b/src/contexts/Auth/AuthProvider.tsx index f2442bf4..21983da5 100644 --- a/src/contexts/Auth/AuthProvider.tsx +++ b/src/contexts/Auth/AuthProvider.tsx @@ -71,7 +71,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { if (profile) updateOrganization(); else - setOrganization(null); + updateOrganization(); + //setOrganization(null); setFinishedLogin(true); return profile; diff --git a/src/pages/Recovery/Recovery.tsx b/src/pages/Recovery/Recovery.tsx index 93274342..83dbf3d7 100644 --- a/src/pages/Recovery/Recovery.tsx +++ b/src/pages/Recovery/Recovery.tsx @@ -3,12 +3,14 @@ import "./Recovery.css" import { transform } from "@babel/core" import { Translate } from "phosphor-react" import "../singin/signinForm.css" -import {useState} from "react" +import {useState, useContext} from "react" import UniversimeApi from "@/services/UniversimeApi" +import { AuthContext } from "@/contexts/Auth/AuthContext"; import * as SwalUtils from "@/utils/sweetalertUtils" import ReCAPTCHA from "react-google-recaptcha-enterprise"; export default function Recovery(){ + const auth = useContext(AuthContext); const [username, setUsername] = useState("") const [msg, setMsg] = useState(null) @@ -31,7 +33,8 @@ export default function Recovery(){ }) } - const ENABLE_RECAPTCHA = import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"; + const ENABLE_RECAPTCHA = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); + const RECAPTCHA_SITE_KEY = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; return(
@@ -57,7 +60,7 @@ export default function Recovery(){ !ENABLE_RECAPTCHA ? null :

- setRecaptchaRef(r) } sitekey={import.meta.env.VITE_RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} /> + setRecaptchaRef(r) } sitekey={RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} />
} diff --git a/src/pages/Settings/EnvironmentsPage/EnvironmentsLoader.ts b/src/pages/Settings/EnvironmentsPage/EnvironmentsLoader.ts new file mode 100644 index 00000000..1c2ecb6e --- /dev/null +++ b/src/pages/Settings/EnvironmentsPage/EnvironmentsLoader.ts @@ -0,0 +1,15 @@ +import { type LoaderFunctionArgs } from "react-router-dom"; +import UniversimeApi from "@/services/UniversimeApi"; + +export type EnvironmentsLoaderResponse = { + envDic: {} | undefined; +}; + +export async function EnvironmentsFetch(): Promise { + const environments = await UniversimeApi.Group.listEnvironments({}); + return { envDic: (environments as any).body.environments??{}, }; +} + +export async function EnvironmentsLoader(args: LoaderFunctionArgs) { + return EnvironmentsFetch(); +} diff --git a/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.less b/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.less new file mode 100644 index 00000000..8e76d9da --- /dev/null +++ b/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.less @@ -0,0 +1,183 @@ +@import url(/src/layouts/colors.less); +@import url(/src/layouts/fonts.less); +@import url(/src/layouts/transitions.less); +@import url(/src/layouts/border-radius.less); + +#environments-settings { + + .environments-list { + margin-top: 1rem; + + .environments-item { + + margin: .5rem; + align-items: center; + + h3 { + margin-top: 1.5rem; + margin-bottom: 1rem; + font-size: 1.2rem; + margin-left: 1rem; + align-items: center; + } + + .environments-label { + display: flex; + font-size: 1rem; + margin-bottom: .5rem; + margin-left: 0; + text-align: left; + align-items: center; + } + + &:not(:last-of-type) { + @margin-size: .75rem; + + padding-bottom: @margin-size; + margin-bottom: @margin-size; + border-bottom: solid 1px @card-item-color; + } + + .enabled-delete-wrapper { + + margin-bottom: .5rem; + display: flex; + justify-content: space-between; + margin-left: 2rem; + align-items: center; + + &:not(:last-of-type) { + @margin-size: .5rem; + + padding-bottom: @margin-size; + margin-bottom: @margin-size; + border-bottom: solid 1px @card-item-color; + } + + .enabled-wrapper { + display: flex; + align-items: center; + + .filter-enabled-root { + + + + @radix-switch-width: 2.5rem; + @radix-switch-height: 1.5rem; + @radix-thumb-diameter: @radix-switch-height; + + width: @radix-switch-width; + height: @radix-switch-height; + margin-right: .5rem; + margin-left: .5rem; + + border-radius: 9999px; + border: none; + outline: 2px solid @primary-color; + + box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.75); + + cursor: pointer; + + &:focus { + outline: 2px solid @secondary-color; + } + + &:not([data-state='checked']) { + background-color: @card-background-color; + } + + &[data-state='checked'] { + background-color: @card-item-color; + + .filter-enabled-thumb { + transform: translateX(calc(@radix-switch-width - @radix-thumb-diameter)); + background-color: @primary-color; + } + } + + .filter-enabled-thumb { + display: block; + width: @radix-thumb-diameter; + height: @radix-thumb-diameter; + background-color: #FFF; + border-radius: 50%; + outline: 2px solid @primary-color; + + transition: transform 100ms; + will-change: transform; + } + + } + + } + + + + .environments-text-wrapper { + @email-font-size: 1rem; + @email-padding: .5rem; + @trigger-width: 9.25em; + + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + align-items: center; + + .environments-text-input { + + align-items: center; + font-size: @email-font-size; + padding: @email-padding; + + background-color: @card-item-color; + border: none; + border-radius: .625rem; + + width: calc(100% + @trigger-width + 10em); + + } + } + } + + } + } + + .buttons-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; + + margin-top: 1rem; + width: 100%; + + > button { + padding: .5em 1em; + border: none; + + font-size: 1rem; + text-transform: uppercase; + font-weight: @font-weight-bold; + + cursor: pointer; + + & + button { + margin-left: 1rem; + } + + &.submit { + color: @font-color-v1; + background-color: @primary-color; + border-radius: .625rem; + + &[disabled] { + background-color: @card-item-color; + color: @font-color-v2; + } + } + } + } +} \ No newline at end of file diff --git a/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.tsx b/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.tsx new file mode 100644 index 00000000..78555e6f --- /dev/null +++ b/src/pages/Settings/EnvironmentsPage/EnvironmentsPage.tsx @@ -0,0 +1,165 @@ +import { useReducer, useState, useEffect } from "react"; +import { useLoaderData, useNavigate } from "react-router-dom"; +import * as Switch from "@radix-ui/react-switch" + +import { SettingsTitle, SettingsDescription } from "@/pages/Settings"; +import UniversimeApi from "@/services/UniversimeApi"; + +import { type EnvironmentsLoaderResponse, EnvironmentsFetch } from "./EnvironmentsLoader"; +import "./EnvironmentsPage.less"; + +export function EnvironmentsPage() { + const data = useLoaderData() as EnvironmentsLoaderResponse; + + const [fetchEnvironmentsItems, setFetchEnvironmentsItems] = useState({}); + const [environmentsItems, setEnvironmentsItems] = useState>([]); + + const [editedItems, setEditedItems] = useReducer((state:any, action:any) => { + switch (action.type) { + case 'RESET': + return {}; + case 'EDIT': + return { ...state, [action.id]: action.value }; + default: + return state; + } + }, {}); + + const canSave = Object.keys(editedItems).length > 0; + + useEffect(() => { + + setFetchEnvironmentsItems(data.envDic); + + setEnvironmentsItems([ + { + title: "Conta", + items: [ + { + name: "Habilitar Registrar-se", + key: "signup_enabled", + type: "boolean", + defaultValue: true, + }, + { + name: "Confirmar Conta ao Registrar-se", + key: "signup_confirm_account_enabled", + type: "boolean", + defaultValue: false, + }, + ] + }, + { + title: "Login via Google", + items: [ + { + name: "Habilitar", + key: "login_google_enabled", + type: "boolean", + defaultValue: false, + }, + { + name: "Client ID", + key: "google_client_id", + type: "string", + defaultValue: "", + }, + ] + }, + { + title: "reCAPTCHA Enterprise", + items: [ + { + name: "Habilitar", + key: "recaptcha_enabled", + type: "boolean", + defaultValue: false, + }, + { + name: "Api Key", + key: "recaptcha_api_key", + type: "string", + defaultValue: "", + }, + { + name: "Api Project Id", + key: "recaptcha_api_project_id", + type: "string", + defaultValue: "", + }, + { + name: "Site Key", + key: "recaptcha_site_key", + type: "string", + defaultValue: "", + }, + ] + }, + ]); + + }, [data]); + + return
+ Variáveis Ambiente + Aqui você pode configurar as variáveis ambiente para algumas funcinalidades da plataforma. + +
+ {environmentsItems.map((section : any) => ( +
+

{section.title}

+ {section.items.map((item : any) => ( +
+
{item.name}
+ { item.type === "boolean" ? ( +
+
+ {getValue(item) ? "Ativado" : "Desativado"} + + + +
+
+ ) : null} + { item.type === "string" ? ( +
+ setTextValue(item, e)} /> +
+ ) : null} +
+ ))} +
+ ))} +
+
+
+ +
+
+ + + function getValue(item: any) { + return editedItems[item.key] !== undefined ? editedItems[item.key] : fetchEnvironmentsItems[item.key] ?? item.defaultValue; + } + + function setTextValue(item: any, event: React.ChangeEvent) { + setEditedItems({ type: 'EDIT', id: item.key, value: event.target.value }); + } + + function makeToggleFilter(item: any) { + return function (checked: boolean) { + setEditedItems({ type: 'EDIT', id: item.key, value: checked }); + }; + } + + async function submitChanges() { + const response = await UniversimeApi.Group.editEnvironments(editedItems); + refreshPage(); + } + + async function refreshPage() { + const newData = await EnvironmentsFetch(); + setFetchEnvironmentsItems(newData.envDic); + setEditedItems({ type: 'RESET' }); + } +} + diff --git a/src/pages/Settings/EnvironmentsPage/index.ts b/src/pages/Settings/EnvironmentsPage/index.ts new file mode 100644 index 00000000..8b071df1 --- /dev/null +++ b/src/pages/Settings/EnvironmentsPage/index.ts @@ -0,0 +1,4 @@ +export { EnvironmentsPage as default } from "./EnvironmentsPage"; + +export * from "./EnvironmentsPage"; +export * from "./EnvironmentsLoader"; diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx index 49af9565..306f5597 100644 --- a/src/pages/Settings/Settings.tsx +++ b/src/pages/Settings/Settings.tsx @@ -19,6 +19,7 @@ export function SettingsPage() { { outlet ?? <> Configurações + diff --git a/src/pages/Settings/index.ts b/src/pages/Settings/index.ts index 442c7fd7..9b9ef815 100644 --- a/src/pages/Settings/index.ts +++ b/src/pages/Settings/index.ts @@ -6,3 +6,4 @@ export * from "./SettingsDescription/SettingsDescription"; export * from "./SettingsOptions/SettingsMoveTo"; export * from "./GroupEmailFilterPage"; export * from "./RolesPage"; +export * from "./EnvironmentsPage"; diff --git a/src/pages/SignUp/SignUpModal/SignUpModal.tsx b/src/pages/SignUp/SignUpModal/SignUpModal.tsx index 3e19f5e9..2c3c10f4 100644 --- a/src/pages/SignUp/SignUpModal/SignUpModal.tsx +++ b/src/pages/SignUp/SignUpModal/SignUpModal.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, FocusEvent, useState, useEffect } from "react"; +import { MouseEvent, FocusEvent, useState, useEffect, useContext } from "react"; import { useNavigate } from "react-router"; import ReCAPTCHA from "react-google-recaptcha-enterprise"; @@ -6,6 +6,7 @@ import { UniversiModal } from "@/components/UniversiModal"; import UniversimeApi from "@/services/UniversimeApi"; import { isEmail } from "@/utils/regexUtils"; import { setStateAsValue } from "@/utils/tsxUtils"; +import { AuthContext } from "@/contexts/Auth/AuthContext"; import { minimumLength, numberOrSpecialChar, passwordValidationClass, upperAndLowerCase } from "@/utils/passwordValidation"; import { enableSignUp } from "./helperFunctions"; import * as SwalUtils from "@/utils/sweetalertUtils"; @@ -22,6 +23,7 @@ const LAST_NAME_MAX_LENGTH = 21; const USERNAME_CHAR_REGEX = /[a-z0-9_.-]/ export function SignUpModal(props: SignUpModalProps) { + const auth = useContext(AuthContext); const navigate = useNavigate(); const [firstname, setFirstname] = useState(""); @@ -81,7 +83,8 @@ export function SignUpModal(props: SignUpModalProps) { return () => clearTimeout(delayDebounceFn) }, [email]) - const ENABLE_RECAPTCHA = import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"; + const ENABLE_RECAPTCHA = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); + const RECAPTCHA_SITE_KEY = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; return ( @@ -173,7 +176,7 @@ export function SignUpModal(props: SignUpModalProps) { !ENABLE_RECAPTCHA ? null :

- setRecaptchaRef(r) } sitekey={import.meta.env.VITE_RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} /> + setRecaptchaRef(r) } sitekey={RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} />
} diff --git a/src/pages/singin/SinginForm.tsx b/src/pages/singin/SinginForm.tsx index 108812b9..db9fc9bf 100644 --- a/src/pages/singin/SinginForm.tsx +++ b/src/pages/singin/SinginForm.tsx @@ -38,9 +38,10 @@ export default function SinginForm() { setShowPassword(!showPassword); }; - const ENABLE_GOOGLE_LOGIN = import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "true" || import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "1"; - const ENABLE_RECAPTCHA = import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"; - + const ENABLE_GOOGLE_LOGIN = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).login_google_enabled ?? (import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "true" || import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "1"); + const ENABLE_RECAPTCHA = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); + const RECAPTCHA_SITE_KEY = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; + return ( <> @@ -85,7 +86,7 @@ export default function SinginForm() { !ENABLE_RECAPTCHA ? null :

- setRecaptchaRef(r) } sitekey={import.meta.env.VITE_RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} /> + setRecaptchaRef(r) } sitekey={RECAPTCHA_SITE_KEY} onChange={handleRecaptchaChange} />
} diff --git a/src/services/UniversimeApi/Group.ts b/src/services/UniversimeApi/Group.ts index 1265ef4b..ff72af0d 100644 --- a/src/services/UniversimeApi/Group.ts +++ b/src/services/UniversimeApi/Group.ts @@ -176,3 +176,11 @@ export async function listEmailFilter(body: GroupIdOrPath_RequestDTO) { groupPath: body.groupPath, })).data; } + +export async function editEnvironments(body: {}) { + return (await api.post("/group/settings/environments/edit", body)).data; +} + +export async function listEnvironments(body: {}) { + return (await api.post("/group/settings/environments/list", body)).data; +} diff --git a/src/services/routes.tsx b/src/services/routes.tsx index 09ec70e8..2113330c 100644 --- a/src/services/routes.tsx +++ b/src/services/routes.tsx @@ -20,7 +20,7 @@ import Recovery from "@/pages/Recovery/Recovery"; import NewPassword from "@/pages/NewPassword/NewPassword"; import ManageProfilePage, { ManageProfileLoader } from "@/pages/ManageProfile"; import Homepage from "@/pages/Homepage"; -import SettingsPage, { GroupEmailFilterPage, GroupEmailFilterLoader, RolesPage, RolesPageLoader } from "@/pages/Settings"; +import SettingsPage, { GroupEmailFilterPage, GroupEmailFilterLoader, RolesPage, RolesPageLoader, EnvironmentsPage, EnvironmentsLoader } from "@/pages/Settings"; @@ -110,6 +110,11 @@ export const router = createBrowserRouter([{ path: "roles", element: , loader: RolesPageLoader, + }, + { + path: "environments", + element: , + loader: EnvironmentsLoader, } ], } From 2023d93b31d9f3d99b868488f2b9669ecfb66d5f Mon Sep 17 00:00:00 2001 From: Julio Verne <55258516+julio-ufpb@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:27:03 -0300 Subject: [PATCH 20/20] change: adjust env signup --- src/pages/Recovery/Recovery.tsx | 5 +++-- src/pages/SignUp/SignUpModal/SignUpModal.tsx | 5 +++-- src/pages/singin/SinginForm.tsx | 11 +++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/pages/Recovery/Recovery.tsx b/src/pages/Recovery/Recovery.tsx index 83dbf3d7..a169f4bc 100644 --- a/src/pages/Recovery/Recovery.tsx +++ b/src/pages/Recovery/Recovery.tsx @@ -33,8 +33,9 @@ export default function Recovery(){ }) } - const ENABLE_RECAPTCHA = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); - const RECAPTCHA_SITE_KEY = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; + const organizationEnv = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any); + const ENABLE_RECAPTCHA = organizationEnv.recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); + const RECAPTCHA_SITE_KEY = organizationEnv.recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; return(
diff --git a/src/pages/SignUp/SignUpModal/SignUpModal.tsx b/src/pages/SignUp/SignUpModal/SignUpModal.tsx index 2c3c10f4..a294a726 100644 --- a/src/pages/SignUp/SignUpModal/SignUpModal.tsx +++ b/src/pages/SignUp/SignUpModal/SignUpModal.tsx @@ -83,8 +83,9 @@ export function SignUpModal(props: SignUpModalProps) { return () => clearTimeout(delayDebounceFn) }, [email]) - const ENABLE_RECAPTCHA = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); - const RECAPTCHA_SITE_KEY = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; + const organizationEnv = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any); + const ENABLE_RECAPTCHA = organizationEnv.recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); + const RECAPTCHA_SITE_KEY = organizationEnv.recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; return ( diff --git a/src/pages/singin/SinginForm.tsx b/src/pages/singin/SinginForm.tsx index db9fc9bf..691c398b 100644 --- a/src/pages/singin/SinginForm.tsx +++ b/src/pages/singin/SinginForm.tsx @@ -38,9 +38,11 @@ export default function SinginForm() { setShowPassword(!showPassword); }; - const ENABLE_GOOGLE_LOGIN = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).login_google_enabled ?? (import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "true" || import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "1"); - const ENABLE_RECAPTCHA = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); - const RECAPTCHA_SITE_KEY = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any).recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; + const organizationEnv = (((auth.organization??{} as any).groupSettings??{} as any).environment??{} as any); + const SIGNUP_ENABLED = organizationEnv.signup_enabled ?? true; + const ENABLE_GOOGLE_LOGIN = organizationEnv.login_google_enabled ?? (import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "true" || import.meta.env.VITE_ENABLE_GOOGLE_LOGIN === "1"); + const ENABLE_RECAPTCHA = organizationEnv.recaptcha_enabled ?? (import.meta.env.VITE_ENABLE_RECAPTCHA === "true" || import.meta.env.VITE_ENABLE_RECAPTCHA === "1"); + const RECAPTCHA_SITE_KEY = organizationEnv.recaptcha_site_key ?? import.meta.env.VITE_RECAPTCHA_SITE_KEY; return ( <> @@ -122,10 +124,11 @@ export default function SinginForm() { } - + { !SIGNUP_ENABLED ? null :
Crie sua conta
+ }
Esqueci minha senha