diff --git a/elm-git.json b/elm-git.json index 96972bc0..ae2fe8c6 100644 --- a/elm-git.json +++ b/elm-git.json @@ -1,7 +1,7 @@ { "git-dependencies": { "direct": { - "https://github.com/unisonweb/ui-core": "194adc73aceca4e3ae1519206e122419c9985f35" + "https://github.com/unisonweb/ui-core": "78ba490fac82e43146be5653e5a9be2288ac6ad0" }, "indirect": {} } diff --git a/elm.json b/elm.json index 5c2d7f21..ed16fd5e 100644 --- a/elm.json +++ b/elm.json @@ -31,6 +31,7 @@ "j-maas/elm-ordered-containers": "1.0.0", "justinmimbs/time-extra": "1.1.1", "krisajenkins/remotedata": "6.0.1", + "kuon/elm-string-normalize": "1.0.6", "mgold/elm-nonempty-list": "4.2.0", "noahzgordon/elm-color-extra": "1.0.2", "rtfeldman/elm-iso8601-date-strings": "1.1.4", diff --git a/src/UnisonShare/Account.elm b/src/UnisonShare/Account.elm index a8d65768..90370d23 100644 --- a/src/UnisonShare/Account.elm +++ b/src/UnisonShare/Account.elm @@ -20,6 +20,7 @@ type alias Account a = , completedTours : List Tour , organizationMemberships : List OrganizationMembership , isSuperAdmin : Bool + , primaryEmail : String } @@ -89,7 +90,7 @@ isProjectOwner projectRef account = decodeSummary : Decode.Decoder AccountSummary decodeSummary = let - makeSummary handle name_ avatarUrl completedTours organizationMemberships isSuperAdmin = + makeSummary handle name_ avatarUrl completedTours organizationMemberships isSuperAdmin primaryEmail = { handle = handle , name = name_ , avatarUrl = avatarUrl @@ -97,12 +98,14 @@ decodeSummary = , completedTours = Maybe.withDefault [] completedTours , organizationMemberships = organizationMemberships , isSuperAdmin = isSuperAdmin + , primaryEmail = primaryEmail } in - Decode.map6 makeSummary + Decode.map7 makeSummary (field "handle" UserHandle.decodeUnprefixed) (maybe (field "name" string)) (maybe (field "avatarUrl" decodeUrl)) (maybe (field "completedTours" (Decode.list Tour.decode))) (field "organizationMemberships" (Decode.list (Decode.map OrganizationMembership UserHandle.decodeUnprefixed))) (field "isSuperadmin" Decode.bool) + (field "primaryEmail" string) diff --git a/src/UnisonShare/Api.elm b/src/UnisonShare/Api.elm index ce493c22..b08de126 100644 --- a/src/UnisonShare/Api.elm +++ b/src/UnisonShare/Api.elm @@ -36,16 +36,26 @@ import UnisonShare.Tour as Tour exposing (Tour) import Url.Builder exposing (QueryParameter, int, string) +profile : UserHandle -> Endpoint +profile handle = + GET { path = [ "users", UserHandle.toUnprefixedString handle ], queryParams = [] } + + user : UserHandle -> Endpoint user handle = GET { path = [ "users", UserHandle.toUnprefixedString handle ], queryParams = [] } +org : UserHandle -> Endpoint +org handle = + GET { path = [ "users", UserHandle.toUnprefixedString handle ], queryParams = [] } + + updateUserProfile : UserHandle -> { bio : String } -> Endpoint -updateUserProfile handle profile = +updateUserProfile handle profile_ = let body = - Encode.object [ ( "bio", Encode.string profile.bio ) ] + Encode.object [ ( "bio", Encode.string profile_.bio ) ] |> Http.jsonBody in PATCH @@ -178,6 +188,25 @@ session = -- ORG ROLE ASSIGNMENTS (COLLABORATORS) +createOrg : { a | handle : UserHandle, primaryEmail : String } -> String -> UserHandle -> Bool -> Endpoint +createOrg owner name orgHandle isCommercial = + let + body = + Encode.object + [ ( "name", Encode.string name ) + , ( "handle", Encode.string (UserHandle.toUnprefixedString orgHandle) ) + , ( "isCommercial", Encode.bool isCommercial ) + , ( "owner", Encode.string (UserHandle.toUnprefixedString owner.handle) ) + , ( "email", Encode.string owner.primaryEmail ) + ] + in + POST + { path = [ "orgs" ] + , queryParams = [] + , body = Http.jsonBody body + } + + orgRoleAssignments : UserHandle -> Endpoint orgRoleAssignments orgHandle = let diff --git a/src/UnisonShare/App.elm b/src/UnisonShare/App.elm index 85851620..fbb8dc93 100644 --- a/src/UnisonShare/App.elm +++ b/src/UnisonShare/App.elm @@ -40,6 +40,7 @@ import UnisonShare.AppDocument as AppDocument import UnisonShare.AppError as AppError exposing (AppError) import UnisonShare.AppHeader as AppHeader import UnisonShare.Link as Link +import UnisonShare.NewOrgModal as NewOrgModal import UnisonShare.Page.AcceptTermsPage as AcceptTermsPage import UnisonShare.Page.AccountPage as AccountPage import UnisonShare.Page.AppErrorPage as AppErrorPage @@ -86,6 +87,7 @@ type Page type AppModal = NoModal | KeyboardShortcuts + | NewOrg NewOrgModal.Model type alias Model = @@ -213,6 +215,7 @@ type Msg | ToggleAccountMenu | ToggleCreateAccountMenu | ShowKeyboardShortcuts + | ShowNewOrgModal | CloseModal | WhatsNewFetchFinished (HttpResult WhatsNew.LoadedWhatsNew) | WhatsNewMarkAllAsRead @@ -224,6 +227,7 @@ type Msg | ProjectPageMsg ProjectPage.Msg | AccountPageMsg AccountPage.Msg | AcceptTermsPageMsg AcceptTermsPage.Msg + | NewOrgModalMsg NewOrgModal.Msg update : Msg -> Model -> ( Model, Cmd Msg ) @@ -465,6 +469,9 @@ update msg ({ appContext } as model) = ( _, ShowKeyboardShortcuts ) -> ( { model | openedAppHeaderMenu = AppHeader.NoneOpened, appModal = KeyboardShortcuts }, Cmd.none ) + ( _, ShowNewOrgModal ) -> + ( { model | openedAppHeaderMenu = AppHeader.NoneOpened, appModal = NewOrg NewOrgModal.init }, Cmd.none ) + ( _, CloseModal ) -> ( { model | appModal = NoModal }, Cmd.none ) @@ -523,6 +530,29 @@ update msg ({ appContext } as model) = in ( { model | page = AcceptTerms continueUrl acceptTerms_ }, Cmd.map AcceptTermsPageMsg acceptTermsCmd ) + ( _, NewOrgModalMsg newOrgMsg ) -> + case ( model.appModal, appContext.session ) of + ( NewOrg newOrg, Session.SignedIn account ) -> + let + ( newOrg_, cmd, out ) = + NewOrgModal.update appContext account newOrgMsg newOrg + + appModal = + case out of + NewOrgModal.NoOutMsg -> + NewOrg newOrg_ + + NewOrgModal.RequestCloseModal -> + NoModal + + NewOrgModal.AddedOrg _ -> + NoModal + in + ( { model | appModal = appModal }, Cmd.map NewOrgModalMsg cmd ) + + _ -> + ( model, Cmd.none ) + _ -> ( model, Cmd.none ) @@ -720,6 +750,7 @@ view model = , toggleAccountMenuMsg = ToggleAccountMenu , toggleCreateAccountMenuMsg = ToggleCreateAccountMenu , showKeyboardShortcutsModalMsg = ShowKeyboardShortcuts + , showNewOrgModal = ShowNewOrgModal } appDocument = @@ -784,6 +815,9 @@ view model = KeyboardShortcuts -> { appDocument | modal = Just (viewKeyboardShortcutsModal appContext.operatingSystem) } + NewOrg m -> + { appDocument | modal = Just (Html.map NewOrgModalMsg (NewOrgModal.view m)) } + appDocumentWithWelcomeTermsModal = -- We link to TermsOfService and PrivacyPolicy from the welcome -- terms modal and the AcceptTerms page is used during UCM signup, diff --git a/src/UnisonShare/AppHeader.elm b/src/UnisonShare/AppHeader.elm index 8d78f5e1..c9be6411 100644 --- a/src/UnisonShare/AppHeader.elm +++ b/src/UnisonShare/AppHeader.elm @@ -5,6 +5,7 @@ import Html.Attributes exposing (class, classList) import Lib.HttpApi exposing (HttpApi) import Lib.UserHandle as UserHandle exposing (UserHandle) import Time +import UI import UI.ActionMenu as ActionMenu import UI.AnchoredOverlay as AnchoredOverlay import UI.AppHeader exposing (AppHeader, AppTitle(..)) @@ -120,6 +121,7 @@ type alias AppHeaderContext msg = , toggleHelpAndResourcesMenuMsg : msg , toggleAccountMenuMsg : msg , toggleCreateAccountMenuMsg : msg + , showNewOrgModal : msg , showKeyboardShortcutsModalMsg : msg } @@ -277,21 +279,21 @@ view ctx appHeader_ = ] ) - SignedIn sesh -> + SignedIn account -> let nav = case activeNavItem of Catalog -> - Navigation.empty |> Navigation.withItems [] navItems.catalog [ navItems.profile sesh.handle ] + Navigation.empty |> Navigation.withItems [] navItems.catalog [ navItems.profile account.handle ] Profile -> - Navigation.empty |> Navigation.withItems [ navItems.catalog ] (navItems.profile sesh.handle) [] + Navigation.empty |> Navigation.withItems [ navItems.catalog ] (navItems.profile account.handle) [] _ -> - Navigation.empty |> Navigation.withNoSelectedItems [ navItems.catalog, navItems.profile sesh.handle ] + Navigation.empty |> Navigation.withNoSelectedItems [ navItems.catalog, navItems.profile account.handle ] avatar = - Avatar.avatar sesh.avatarUrl (Maybe.map (String.left 1) sesh.name) + Avatar.avatar account.avatarUrl (Maybe.map (String.left 1) account.name) |> Avatar.view viewAccountMenuTrigger isOpen = @@ -306,6 +308,16 @@ view ctx appHeader_ = div [ classList [ ( "account-menu-trigger", True ), ( "account-menu_is-open", isOpen ) ] ] [ avatar, Icon.view chevron ] + newOrgButton = + if account.isSuperAdmin then + Button.iconThenLabel ctx.showNewOrgModal Icon.largePlus "New Org" + |> Button.small + |> Button.positive + |> Button.view + + else + UI.nothing + accountMenu = ActionMenu.items (ActionMenu.optionItem Icon.cog "Account Settings" Link.account) @@ -315,7 +327,7 @@ view ctx appHeader_ = |> ActionMenu.view |> (\a -> div [ class "account-menu" ] [ a ]) in - ( nav, [ helpAndResources, accountMenu ] ) + ( nav, [ newOrgButton, helpAndResources, accountMenu ] ) in UI.AppHeader.appHeader (appTitle (Click.href "/")) |> UI.AppHeader.withNavigation navigation diff --git a/src/UnisonShare/NewOrgModal.elm b/src/UnisonShare/NewOrgModal.elm new file mode 100644 index 00000000..fe50cc4e --- /dev/null +++ b/src/UnisonShare/NewOrgModal.elm @@ -0,0 +1,425 @@ +module UnisonShare.NewOrgModal exposing (..) + +import Html exposing (Html, div, form) +import Http +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Lib.Util as Util +import RemoteData exposing (RemoteData(..), WebData) +import String.Normalize as StringN +import UI.Button as Button +import UI.Form.RadioField as RadioField +import UI.Form.TextField as TextField +import UI.Icon as Icon +import UI.Modal as Modal +import UI.StatusBanner as StatusBanner +import UI.StatusIndicator as StatusIndicator +import UnisonShare.Account exposing (AccountSummary) +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Org as Org exposing (OrgSummary) + + +type OrgType + = PublicOrg + | CommercialOrg + + +type OrgHandle + = Blank + | Suggested UserHandle + | UserEntered String + | CheckingAvailability { handle : UserHandle, isSuggested : Bool } + | NotAvailable { handle : UserHandle, isSuggested : Bool } + | Available { handle : UserHandle, isSuggested : Bool } + + +type Validity + = NotCheckedValidity + | Valid + | Invalid { needsName : Bool, needsHandle : Bool } + + +type alias Model = + { name : String + , potentialHandle : OrgHandle + , orgType : OrgType + , validity : Validity + , save : WebData OrgSummary + } + + +init : Model +init = + { name = "" + , potentialHandle = Blank + , orgType = PublicOrg + , validity = NotCheckedValidity + , save = NotAsked + } + + + +-- UPDATE + + +type Msg + = CloseModal + | UpdateName String + | UpdateHandle String + | UpdateOrgType OrgType + | CheckHandleAvailability UserHandle + | HandleAvailabilityCheckFinished UserHandle Bool + | Save + | SaveFinished (HttpResult OrgSummary) + + +type OutMsg + = NoOutMsg + | RequestCloseModal + | AddedOrg OrgSummary + + +update : AppContext -> AccountSummary -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext account msg model = + case msg of + UpdateName name -> + let + suggestedHandle = + toSuggestedHandle name + + suggestAndCheck h isSuggested = + if isSuggested then + ( Suggested h + , Util.delayMsg 250 (CheckHandleAvailability h) + ) + + else + ( model.potentialHandle, Cmd.none ) + + ( potentialHandle, checkCmd ) = + case ( suggestedHandle, model.potentialHandle ) of + ( Just h, Blank ) -> + suggestAndCheck h True + + ( Just h, Suggested _ ) -> + suggestAndCheck h True + + ( Just h, NotAvailable { isSuggested } ) -> + suggestAndCheck h isSuggested + + ( Just h, Available { isSuggested } ) -> + suggestAndCheck h isSuggested + + _ -> + ( model.potentialHandle, Cmd.none ) + in + ( { model + | validity = NotCheckedValidity + , name = name + , potentialHandle = potentialHandle + } + , checkCmd + , NoOutMsg + ) + + UpdateHandle handleInput -> + let + cmd = + handleInput + |> String.replace "@" "" + |> UserHandle.fromUnprefixedString + |> Maybe.map (\h -> Util.delayMsg 250 (CheckHandleAvailability h)) + |> Maybe.withDefault Cmd.none + + potentialHandle = + if String.isEmpty handleInput then + Blank + + else + UserEntered handleInput + in + ( { model + | validity = NotCheckedValidity + , potentialHandle = potentialHandle + } + , cmd + , NoOutMsg + ) + + UpdateOrgType orgType -> + ( { model | validity = NotCheckedValidity, orgType = orgType }, Cmd.none, NoOutMsg ) + + CheckHandleAvailability handle -> + let + ( model_, cmd ) = + case model.potentialHandle of + Suggested handle_ -> + if UserHandle.equals handle handle_ then + ( { model | potentialHandle = CheckingAvailability { handle = handle_, isSuggested = True } } + , checkHandleAvailability appContext handle_ + ) + + else + ( model, Cmd.none ) + + UserEntered rawHandle -> + case (String.replace "@" "" >> UserHandle.fromUnprefixedString) rawHandle of + Just h_ -> + if UserHandle.equals handle h_ then + ( { model | potentialHandle = CheckingAvailability { handle = h_, isSuggested = False } } + , checkHandleAvailability appContext h_ + ) + + else + ( model, Cmd.none ) + + Nothing -> + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) + in + ( model_, cmd, NoOutMsg ) + + HandleAvailabilityCheckFinished handle isAvailable -> + let + handle_ = + case model.potentialHandle of + CheckingAvailability checking -> + if handle == checking.handle then + if isAvailable then + Available checking + + else + NotAvailable checking + + else + model.potentialHandle + + _ -> + model.potentialHandle + in + ( { model | potentialHandle = handle_ }, Cmd.none, NoOutMsg ) + + Save -> + let + model_ = + validateForm model + in + case ( model_.validity, UserHandle.fromUnprefixedString (handleToString model_.potentialHandle) ) of + ( Valid, Just handle ) -> + ( { model_ | save = Loading } + , saveOrg appContext account model.name handle model.orgType + , NoOutMsg + ) + + _ -> + ( model_, Cmd.none, NoOutMsg ) + + SaveFinished res -> + case res of + Ok org -> + ( { model | save = Success org }, Util.delayMsg 1500 CloseModal, NoOutMsg ) + + Err e -> + ( { model | save = Failure e }, Cmd.none, NoOutMsg ) + + CloseModal -> + ( model, Cmd.none, RequestCloseModal ) + + +toSuggestedHandle : String -> Maybe UserHandle +toSuggestedHandle s = + s |> StringN.slug |> UserHandle.fromUnprefixedString + + +isSuggestedHandle : OrgHandle -> Bool +isSuggestedHandle h = + case h of + Suggested _ -> + True + + _ -> + False + + +validateForm : Model -> Model +validateForm model = + let + handle = + UserHandle.fromUnprefixedString + (handleToString model.potentialHandle) + + validity = + case ( not (String.isEmpty model.name), handle ) of + ( True, Just _ ) -> + Valid + + ( True, Nothing ) -> + Invalid { needsName = False, needsHandle = True } + + ( False, Just _ ) -> + Invalid { needsName = True, needsHandle = False } + + ( False, Nothing ) -> + Invalid { needsName = True, needsHandle = True } + in + { model | validity = validity } + + + +-- EFFECTS + + +checkHandleAvailability : AppContext -> UserHandle -> Cmd Msg +checkHandleAvailability appContext h = + let + isAvailable res = + case res of + Err (Http.BadStatus 404) -> + True + + _ -> + False + in + ShareApi.profile h + |> HttpApi.toRequest + (Decode.succeed False) + (isAvailable >> HandleAvailabilityCheckFinished h) + |> HttpApi.perform appContext.api + + +saveOrg : AppContext -> AccountSummary -> String -> UserHandle -> OrgType -> Cmd Msg +saveOrg appContext account name handle orgType = + ShareApi.createOrg account name handle (orgType == CommercialOrg) + |> HttpApi.toRequest Org.decodeSummary SaveFinished + |> HttpApi.perform appContext.api + + + +-- VIEW + + +handleToString : OrgHandle -> String +handleToString orgHandle = + case orgHandle of + Blank -> + "" + + Suggested h -> + UserHandle.toUnprefixedString h + + UserEntered h -> + h + + CheckingAvailability { handle } -> + UserHandle.toUnprefixedString handle + + NotAvailable { handle } -> + UserHandle.toUnprefixedString handle + + Available { handle } -> + UserHandle.toUnprefixedString handle + + +view : Model -> Html Msg +view model = + let + orgTypeOptions = + RadioField.options2 + (RadioField.option "Public Org" + "Only allows public projects." + PublicOrg + ) + (RadioField.option "Commercial Org" + "Supports both public and private projects. Selecting this will open a support ticket for the enablement of private projects." + CommercialOrg + ) + + handleField = + TextField.fieldWithoutLabel UpdateHandle "Handle, e.g. @unison" (handleToString model.potentialHandle) + |> TextField.withHelpText "The unique identifier of the organization and used in URLs and project references like @unison/base." + |> TextField.withIcon Icon.at + + handleField_ = + case model.potentialHandle of + CheckingAvailability _ -> + handleField + |> TextField.withStatusIndicator StatusIndicator.working + + Available _ -> + handleField + |> TextField.withStatusIndicator StatusIndicator.good + + NotAvailable _ -> + handleField + |> TextField.withHelpText "This handle is currently taken by another user or organization." + |> TextField.withStatusIndicator StatusIndicator.bad + |> TextField.markAsInvalid + + _ -> + let + h = + handleToString model.potentialHandle + in + if UserHandle.isValidHandle h || String.length h <= 2 then + handleField + + else + handleField + |> TextField.withHelpText + "May only contain alphanumeric characters or hyphens. Can't have multiple consecutive hyphens. Can't begin or end with a hyphen. Max 39 characters." + |> TextField.markAsInvalid + + content = + div [] + [ form [] + [ TextField.fieldWithoutLabel UpdateName "Name, e.g. Unison" model.name + |> TextField.withIcon Icon.tag + |> TextField.withAutofocus + |> TextField.view + , TextField.view handleField_ + , RadioField.field "org-type" UpdateOrgType orgTypeOptions model.orgType |> RadioField.view + ] + ] + + modal = + Modal.content content + |> Modal.modal "new-org-modal" CloseModal + |> Modal.withHeader "New Organization" + |> Modal.withActions + [ Button.button CloseModal "Cancel" + |> Button.subdued + , Button.button Save "Create Org" + |> Button.positive + ] + + modal_ = + case model.save of + NotAsked -> + case model.validity of + Invalid _ -> + Modal.withLeftSideFooter + [ StatusBanner.bad "Please provide a name and a handle" ] + modal + + _ -> + modal + + Loading -> + modal + |> Modal.withDimOverlay True + |> Modal.withLeftSideFooter + [ StatusBanner.working "Saving..." ] + + Success _ -> + modal + + Failure _ -> + Modal.withLeftSideFooter + [ StatusBanner.bad "Error, could not save Org. Try again." ] + modal + in + Modal.view modal_ diff --git a/src/UnisonShare/Page/OrgPage.elm b/src/UnisonShare/Page/OrgPage.elm index fc81d254..7c172eba 100644 --- a/src/UnisonShare/Page/OrgPage.elm +++ b/src/UnisonShare/Page/OrgPage.elm @@ -102,7 +102,7 @@ updateSubPage _ _ model _ = fetchOrg : AppContext -> UserHandle -> Cmd Msg fetchOrg appContext handle = - ShareApi.user handle + ShareApi.org handle |> HttpApi.toRequest Org.decodeSummary (RemoteData.fromResult >> FetchOrgFinished) |> HttpApi.perform appContext.api diff --git a/src/UnisonShare/PreApp.elm b/src/UnisonShare/PreApp.elm index acbe98b2..23df1f3e 100644 --- a/src/UnisonShare/PreApp.elm +++ b/src/UnisonShare/PreApp.elm @@ -127,20 +127,6 @@ fetchSession preAppContext = |> Task.onError onError - -{- - fetchSession : PreAppContext -> Cmd Msg - fetchSession preAppContext = - let - api = - HttpApi.httpApi True preAppContext.flags.apiUrl preAppContext.flags.xsrfToken - in - ShareApi.session - |> HttpApi.toRequest Session.decode FetchSessionFinished - |> HttpApi.perform api --} - - subscriptions : Model -> Sub Msg subscriptions model = case model of diff --git a/src/css/unison-share.css b/src/css/unison-share.css index 11b2b839..a99267a6 100644 --- a/src/css/unison-share.css +++ b/src/css/unison-share.css @@ -1,6 +1,7 @@ @import "./confirm-delete.css"; @import "./unison-share/app.css"; @import "./unison-share/info-modal.css"; +@import "./unison-share/new-org-modal.css"; @import "./unison-share/welcome-tour-modal.css"; @import "./unison-share/banner.css"; @import "./unison-share/help-modal.css"; diff --git a/src/css/unison-share/new-org-modal.css b/src/css/unison-share/new-org-modal.css new file mode 100644 index 00000000..5f58722a --- /dev/null +++ b/src/css/unison-share/new-org-modal.css @@ -0,0 +1,14 @@ +#new-org-modal { + width: 34rem; +} + +#new-org-modal form { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1rem; +} + +#new-org-modal form .radio-field .radio-field_option { + max-width: auto; +} diff --git a/tests/e2e/NewOrgModal.spec.ts b/tests/e2e/NewOrgModal.spec.ts new file mode 100644 index 00000000..e9594be0 --- /dev/null +++ b/tests/e2e/NewOrgModal.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from "@playwright/test"; +import { button } from "./TestHelpers/Page"; +import * as API from "./TestHelpers/Api"; + +test.beforeEach(async ({ page }) => { + await API.getWebsiteFeed(page); + await API.getCatalog(page); +}); + +test.describe("without being signed in", () => { + test.beforeEach(async ({ page }) => { + await API.getAccount(page, "NOT_SIGNED_IN"); + }); + + test("can *NOT* see the 'New Org' button", async ({ page }) => { + const response = await page.goto("http://localhost:1234"); + expect(response?.status()).toBeLessThan(400); + + await expect(button(page, "New Org")).not.toBeVisible(); + }); +}); + +test.describe("while being signed in", () => { + test.beforeEach(async ({ page }) => { + await API.getAccount(page, "@alice", { isSuperadmin: true }); + }); + + test("can see the 'New Org' button", async ({ page }) => { + const response = await page.goto("http://localhost:1234"); + expect(response?.status()).toBeLessThan(400); + + await expect(button(page, "New Org")).toBeVisible(); + }); +}); diff --git a/tests/e2e/OrgPeoplePage.spec.ts b/tests/e2e/OrgPeoplePage.spec.ts index 71873c2c..75761ffa 100644 --- a/tests/e2e/OrgPeoplePage.spec.ts +++ b/tests/e2e/OrgPeoplePage.spec.ts @@ -25,7 +25,7 @@ test.describe("without being signed in", () => { await expect(handle).toBeVisible(); const title = page.getByText("You're not authorized to view this page"); - expect(title).toBeVisible(); + await expect(title).toBeVisible(); }); }); @@ -48,7 +48,7 @@ test.describe("without org:manage permission", () => { await expect(handle).toBeVisible(); const title = page.getByText("You're not authorized to view this page"); - expect(title).toBeVisible(); + await expect(title).toBeVisible(); }); }); diff --git a/tests/e2e/TestHelpers/Data.ts b/tests/e2e/TestHelpers/Data.ts index ae00dfb0..5f7a7c9b 100644 --- a/tests/e2e/TestHelpers/Data.ts +++ b/tests/e2e/TestHelpers/Data.ts @@ -3,9 +3,9 @@ import { faker } from "@faker-js/faker"; function account(handle: string) { return { ...user(), - handle, + handle: handle.replace("@", ""), completedTours: ["welcome-terms"], - isSuperadmin: faker.datatype.boolean(), + isSuperadmin: false, organizationMemberships: [], primaryEmail: faker.internet.email(), }; diff --git a/webpack.dev.js b/webpack.dev.js index 34696814..0e34bbd4 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -221,7 +221,9 @@ module.exports = { }, { context: ["/website"], - target: WEBSITE_URL, + bypass: (req, res, _proxyOptions) => { + return []; + }, pathRewrite: { "^/website": "" }, logLevel: "debug", },