mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27:52 +01:00
Add tour and other improvements (#348)
* Add tour * Add instructions to set password for OAuth users * Tenant signup improvement * Fix bug on user soft delete * Slighlty darken background color * Add a stronger confirmation for board deletion
This commit is contained in:
committed by
GitHub
parent
57ecddb035
commit
f0346a73ec
BIN
app/assets/images/feedback-space-created.png
Normal file
BIN
app/assets/images/feedback-space-created.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -96,4 +96,9 @@
|
|||||||
color: white !important;
|
color: white !important;
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-header {
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--astuto-grey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -315,4 +315,12 @@ body {
|
|||||||
.alert-warning,
|
.alert-warning,
|
||||||
.text-center,
|
.text-center,
|
||||||
.m-0;
|
.m-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoBanner {
|
||||||
|
@extend
|
||||||
|
.alert,
|
||||||
|
.alert-secondary,
|
||||||
|
.text-center,
|
||||||
|
.m-0;
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebarFilters {
|
||||||
|
@extend .m-0, .p-0;
|
||||||
|
}
|
||||||
|
|
||||||
.postStatusListItemContainer {
|
.postStatusListItemContainer {
|
||||||
@extend
|
@extend
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
.editUser {
|
.editUser {
|
||||||
display: block;
|
display: block;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin-top: 4px;
|
margin-top: 8px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
:root {
|
:root {
|
||||||
// Theme palette (supposed to be customized)
|
// Theme palette (supposed to be customized)
|
||||||
--primary-color: rgb(51, 51, 51);
|
--primary-color: rgb(51, 51, 51);
|
||||||
--background-color: rgb(255, 255, 255);
|
--background-color: rgb(251, 251, 251);
|
||||||
|
|
||||||
// Theme palette shades (supposed to be computed from theme palette)
|
// Theme palette shades (supposed to be computed from theme palette)
|
||||||
--primary-color-light: color-mix(in srgb,var(--primary-color), #fff 85%);
|
--primary-color-light: color-mix(in srgb,var(--primary-color), #fff 85%);
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ class ApplicationController < ActionController::Base
|
|||||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||||
prepend_before_action :load_tenant_data
|
prepend_before_action :load_tenant_data
|
||||||
|
|
||||||
|
# Override Devise after sign in path
|
||||||
|
def after_sign_in_path_for(resource)
|
||||||
|
if resource.admin? && resource.sign_in_count == 1
|
||||||
|
root_path(tour: true)
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def configure_permitted_parameters
|
def configure_permitted_parameters
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class OAuthsController < ApplicationController
|
|||||||
|
|
||||||
elsif reason == 'tenantsignup'
|
elsif reason == 'tenantsignup'
|
||||||
|
|
||||||
@o_auths = @o_auths = OAuth.unscoped.where(tenant_id: nil, is_enabled: true)
|
@o_auths = OAuth.unscoped.where(tenant_id: nil, is_enabled: true)
|
||||||
|
|
||||||
@user_email = query_path_from_object(user_profile, @o_auth.json_user_email_path)
|
@user_email = query_path_from_object(user_profile, @o_auth.json_user_email_path)
|
||||||
if not @o_auth.json_user_name_path.blank?
|
if not @o_auth.json_user_name_path.blank?
|
||||||
@@ -106,6 +106,7 @@ class OAuthsController < ApplicationController
|
|||||||
|
|
||||||
session[:o_auth_sign_up] = "#{@user_email},#{@user_name}"
|
session[:o_auth_sign_up] = "#{@user_email},#{@user_name}"
|
||||||
|
|
||||||
|
@page_title = "Create your feedback space"
|
||||||
render 'tenants/new'
|
render 'tenants/new'
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ class RegistrationsController < Devise::RegistrationsController
|
|||||||
# Override destroy to soft delete
|
# Override destroy to soft delete
|
||||||
def destroy
|
def destroy
|
||||||
resource.status = "deleted"
|
resource.status = "deleted"
|
||||||
resource.email = ''
|
resource.email = "#{SecureRandom.alphanumeric(16)}@deleted.com"
|
||||||
resource.full_name = t('defaults.deleted_user_full_name')
|
resource.full_name = t('defaults.deleted_user_full_name')
|
||||||
|
resource.skip_confirmation
|
||||||
resource.save
|
resource.save
|
||||||
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
|
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
|
||||||
set_flash_message :notice, :destroyed
|
set_flash_message :notice, :destroyed
|
||||||
|
|||||||
@@ -112,33 +112,35 @@ class BoardP extends React.Component<Props> {
|
|||||||
isLoggedIn={isLoggedIn}
|
isLoggedIn={isLoggedIn}
|
||||||
authenticityToken={authenticityToken}
|
authenticityToken={authenticityToken}
|
||||||
/>
|
/>
|
||||||
<SearchFilter
|
<div className="sidebarFilters">
|
||||||
searchQuery={filters.searchQuery}
|
<SearchFilter
|
||||||
handleChange={handleSearchFilterChange}
|
searchQuery={filters.searchQuery}
|
||||||
/>
|
handleChange={handleSearchFilterChange}
|
||||||
{
|
|
||||||
isPowerUser &&
|
|
||||||
<>
|
|
||||||
<SortByFilter
|
|
||||||
sortBy={filters.sortBy}
|
|
||||||
handleChange={sortBy => handleSortByFilterChange(sortBy)}
|
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
isPowerUser &&
|
||||||
|
<>
|
||||||
|
<SortByFilter
|
||||||
|
sortBy={filters.sortBy}
|
||||||
|
handleChange={sortBy => handleSortByFilterChange(sortBy)}
|
||||||
|
/>
|
||||||
|
|
||||||
<DateFilter
|
<DateFilter
|
||||||
startDate={filters.date.startDate}
|
startDate={filters.date.startDate}
|
||||||
endDate={filters.date.endDate}
|
endDate={filters.date.endDate}
|
||||||
handleChange={handleDateFilterChange}
|
handleChange={handleDateFilterChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
<PostStatusFilter
|
||||||
|
postStatuses={postStatuses.items}
|
||||||
|
areLoading={postStatuses.areLoading}
|
||||||
|
error={postStatuses.error}
|
||||||
|
|
||||||
|
currentFilter={filters.postStatusIds}
|
||||||
|
handleFilterClick={handlePostStatusFilterChange}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
}
|
|
||||||
<PostStatusFilter
|
|
||||||
postStatuses={postStatuses.items}
|
|
||||||
areLoading={postStatuses.areLoading}
|
|
||||||
error={postStatuses.error}
|
|
||||||
|
|
||||||
currentFilter={filters.postStatusIds}
|
|
||||||
handleFilterClick={handlePostStatusFilterChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{ tenantSetting.show_powered_by && <PoweredByLink /> }
|
{ tenantSetting.show_powered_by && <PoweredByLink /> }
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class BoardsEditable extends React.Component<Props, State> {
|
|||||||
</ActionLink>
|
</ActionLink>
|
||||||
|
|
||||||
<ActionLink
|
<ActionLink
|
||||||
onClick={() => confirm(I18n.t('common.confirmation')) && handleDelete(id)}
|
onClick={() => confirm(I18n.t('common.confirmation_board_delete', { board: name }) + " " + I18n.t('common.confirmation')) && handleDelete(id)}
|
||||||
icon={<DeleteIcon />}
|
icon={<DeleteIcon />}
|
||||||
customClass="deleteAction"
|
customClass="deleteAction"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface Props {
|
|||||||
pendingTenantImage: string;
|
pendingTenantImage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfirmSignUpPage = ({
|
const ConfirmEmailSignUpPage = ({
|
||||||
subdomain,
|
subdomain,
|
||||||
userEmail,
|
userEmail,
|
||||||
pendingTenantImage,
|
pendingTenantImage,
|
||||||
@@ -23,4 +23,4 @@ const ConfirmSignUpPage = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ConfirmSignUpPage;
|
export default ConfirmEmailSignUpPage;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '../common/Box';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
baseUrl: string;
|
||||||
|
subdomain: string;
|
||||||
|
feedbackSpaceCreatedImage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfirmOAuthSignUpPage = ({
|
||||||
|
baseUrl,
|
||||||
|
subdomain,
|
||||||
|
feedbackSpaceCreatedImage,
|
||||||
|
}: Props) => {
|
||||||
|
let redirectUrl = new URL(baseUrl);
|
||||||
|
redirectUrl.hostname = `${subdomain}.${redirectUrl.hostname}`;
|
||||||
|
redirectUrl.pathname = '/users/sign_in';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<h3>You're all set!</h3>
|
||||||
|
|
||||||
|
<img src={feedbackSpaceCreatedImage} width={64} height={64} style={{margin: '12px auto'}} />
|
||||||
|
|
||||||
|
<p style={{textAlign: 'center'}}>
|
||||||
|
You'll be redirected to your feedback space in a few seconds.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style={{textAlign: 'center'}}>
|
||||||
|
If you are not redirected, please <a href={redirectUrl.toString()} className="link">click here</a>.
|
||||||
|
</p>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmOAuthSignUpPage;
|
||||||
@@ -15,6 +15,7 @@ interface Props {
|
|||||||
handleSignUpSubmit(siteName: string, subdomain: string): void;
|
handleSignUpSubmit(siteName: string, subdomain: string): void;
|
||||||
trialPeriodDays: number;
|
trialPeriodDays: number;
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
|
setCurrentStep(step: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TenantSignUpForm = ({
|
const TenantSignUpForm = ({
|
||||||
@@ -23,6 +24,7 @@ const TenantSignUpForm = ({
|
|||||||
handleSignUpSubmit,
|
handleSignUpSubmit,
|
||||||
trialPeriodDays,
|
trialPeriodDays,
|
||||||
currentStep,
|
currentStep,
|
||||||
|
setCurrentStep,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { register, handleSubmit, formState: { errors } } = useForm<ITenantSignUpTenantForm>();
|
const { register, handleSubmit, formState: { errors } } = useForm<ITenantSignUpTenantForm>();
|
||||||
const onSubmit: SubmitHandler<ITenantSignUpTenantForm> = data => {
|
const onSubmit: SubmitHandler<ITenantSignUpTenantForm> = data => {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import HttpStatus from '../../constants/http_status';
|
|
||||||
import ConfirmSignUpPage from './ConfirmSignUpPage';
|
|
||||||
|
|
||||||
import TenantSignUpForm from './TenantSignUpForm';
|
import TenantSignUpForm from './TenantSignUpForm';
|
||||||
import UserSignUpForm from './UserSignUpForm';
|
import UserSignUpForm from './UserSignUpForm';
|
||||||
|
import ConfirmEmailSignUpPage from './ConfirmEmailSignUpPage';
|
||||||
|
import ConfirmOAuthSignUpPage from './ConfirmOAuthSignUpPage';
|
||||||
import { IOAuth } from '../../interfaces/IOAuth';
|
import { IOAuth } from '../../interfaces/IOAuth';
|
||||||
|
import HttpStatus from '../../constants/http_status';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
oAuthLoginCompleted: boolean;
|
oAuthLoginCompleted: boolean;
|
||||||
@@ -27,6 +28,7 @@ interface Props {
|
|||||||
): Promise<any>;
|
): Promise<any>;
|
||||||
|
|
||||||
astutoLogoImage: string;
|
astutoLogoImage: string;
|
||||||
|
feedbackSpaceCreatedImage: string;
|
||||||
pendingTenantImage: string;
|
pendingTenantImage: string;
|
||||||
|
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@@ -58,6 +60,7 @@ const TenantSignUpP = ({
|
|||||||
error,
|
error,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
astutoLogoImage,
|
astutoLogoImage,
|
||||||
|
feedbackSpaceCreatedImage,
|
||||||
pendingTenantImage,
|
pendingTenantImage,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
trialPeriodDays,
|
trialPeriodDays,
|
||||||
@@ -66,6 +69,9 @@ const TenantSignUpP = ({
|
|||||||
// authMethod is either 'none', 'email' or 'oauth'
|
// authMethod is either 'none', 'email' or 'oauth'
|
||||||
const [authMethod, setAuthMethod] = useState<AuthMethod>(oAuthLoginCompleted ? 'oauth' : 'none');
|
const [authMethod, setAuthMethod] = useState<AuthMethod>(oAuthLoginCompleted ? 'oauth' : 'none');
|
||||||
|
|
||||||
|
// goneBack is set to true if the user goes back from the tenant form to the user form
|
||||||
|
const [goneBack, setGoneBack] = useState(false);
|
||||||
|
|
||||||
const [userData, setUserData] = useState({
|
const [userData, setUserData] = useState({
|
||||||
fullName: oAuthLoginCompleted ? oauthUserName : '',
|
fullName: oAuthLoginCompleted ? oauthUserName : '',
|
||||||
email: oAuthLoginCompleted ? oauthUserEmail : '',
|
email: oAuthLoginCompleted ? oauthUserEmail : '',
|
||||||
@@ -91,15 +97,22 @@ const TenantSignUpP = ({
|
|||||||
authenticityToken,
|
authenticityToken,
|
||||||
).then(res => {
|
).then(res => {
|
||||||
if (res?.status !== HttpStatus.Created) return;
|
if (res?.status !== HttpStatus.Created) return;
|
||||||
|
|
||||||
|
setTenantData({ siteName, subdomain });
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
|
||||||
if (authMethod == 'oauth') {
|
if (authMethod == 'oauth') {
|
||||||
let redirectUrl = new URL(baseUrl);
|
let redirectUrl = new URL(baseUrl);
|
||||||
redirectUrl.hostname = `${subdomain}.${redirectUrl.hostname}`;
|
redirectUrl.hostname = `${subdomain}.${redirectUrl.hostname}`;
|
||||||
window.location.href = redirectUrl.toString();
|
redirectUrl.pathname = '/users/sign_in';
|
||||||
|
|
||||||
|
// redirect after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = redirectUrl.toString();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTenantData({ siteName, subdomain });
|
|
||||||
setCurrentStep(currentStep + 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,23 +131,34 @@ const TenantSignUpP = ({
|
|||||||
oAuths={oAuths}
|
oAuths={oAuths}
|
||||||
userData={userData}
|
userData={userData}
|
||||||
setUserData={setUserData}
|
setUserData={setUserData}
|
||||||
|
setGoneBack={setGoneBack}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
(currentStep === 1 || currentStep === 2) &&
|
(goneBack || currentStep === 2) &&
|
||||||
<TenantSignUpForm
|
<TenantSignUpForm
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
error={error}
|
error={error}
|
||||||
handleSignUpSubmit={handleSignUpSubmit}
|
handleSignUpSubmit={handleSignUpSubmit}
|
||||||
trialPeriodDays={trialPeriodDays}
|
trialPeriodDays={trialPeriodDays}
|
||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
|
setCurrentStep={setCurrentStep}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
currentStep === 3 &&
|
currentStep === 3 && authMethod === 'oauth' &&
|
||||||
<ConfirmSignUpPage
|
<ConfirmOAuthSignUpPage
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
subdomain={tenantData.subdomain}
|
||||||
|
feedbackSpaceCreatedImage={feedbackSpaceCreatedImage}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
currentStep === 3 && authMethod === 'email' &&
|
||||||
|
<ConfirmEmailSignUpPage
|
||||||
subdomain={tenantData.subdomain}
|
subdomain={tenantData.subdomain}
|
||||||
userEmail={userData.email}
|
userEmail={userData.email}
|
||||||
pendingTenantImage={pendingTenantImage}
|
pendingTenantImage={pendingTenantImage}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface Props {
|
|||||||
oAuths: Array<IOAuth>;
|
oAuths: Array<IOAuth>;
|
||||||
userData: ITenantSignUpUserForm;
|
userData: ITenantSignUpUserForm;
|
||||||
setUserData({}: ITenantSignUpUserForm): void;
|
setUserData({}: ITenantSignUpUserForm): void;
|
||||||
|
setGoneBack(goneBack: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserSignUpForm = ({
|
const UserSignUpForm = ({
|
||||||
@@ -31,6 +32,7 @@ const UserSignUpForm = ({
|
|||||||
oAuths,
|
oAuths,
|
||||||
userData,
|
userData,
|
||||||
setUserData,
|
setUserData,
|
||||||
|
setGoneBack,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -164,7 +166,16 @@ const UserSignUpForm = ({
|
|||||||
currentStep === 2 &&
|
currentStep === 2 &&
|
||||||
<p className="userRecap">
|
<p className="userRecap">
|
||||||
<b>{userData.fullName}</b> ({userData.email})
|
<b>{userData.fullName}</b> ({userData.email})
|
||||||
<ActionLink onClick={() => setCurrentStep(currentStep-1)} icon={<EditIcon />} customClass="editUser">Edit</ActionLink>
|
<ActionLink
|
||||||
|
onClick={() => {
|
||||||
|
setGoneBack(true);
|
||||||
|
setCurrentStep(currentStep-1);
|
||||||
|
}}
|
||||||
|
icon={<EditIcon />}
|
||||||
|
customClass="editUser"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</ActionLink>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface Props {
|
|||||||
oauthUserName?: string;
|
oauthUserName?: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
astutoLogoImage: string;
|
astutoLogoImage: string;
|
||||||
|
feedbackSpaceCreatedImage: string;
|
||||||
pendingTenantImage: string;
|
pendingTenantImage: string;
|
||||||
trialPeriodDays: number;
|
trialPeriodDays: number;
|
||||||
authenticityToken: string;
|
authenticityToken: string;
|
||||||
@@ -35,6 +36,7 @@ class TenantSignUpRoot extends React.Component<Props> {
|
|||||||
oauthUserEmail,
|
oauthUserEmail,
|
||||||
oauthUserName,
|
oauthUserName,
|
||||||
astutoLogoImage,
|
astutoLogoImage,
|
||||||
|
feedbackSpaceCreatedImage,
|
||||||
pendingTenantImage,
|
pendingTenantImage,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
trialPeriodDays,
|
trialPeriodDays,
|
||||||
@@ -49,6 +51,7 @@ class TenantSignUpRoot extends React.Component<Props> {
|
|||||||
oauthUserName={oauthUserName}
|
oauthUserName={oauthUserName}
|
||||||
oAuths={oAuths.map(oAuth => oAuthJSON2JS(oAuth))}
|
oAuths={oAuths.map(oAuth => oAuthJSON2JS(oAuth))}
|
||||||
astutoLogoImage={astutoLogoImage}
|
astutoLogoImage={astutoLogoImage}
|
||||||
|
feedbackSpaceCreatedImage={feedbackSpaceCreatedImage}
|
||||||
pendingTenantImage={pendingTenantImage}
|
pendingTenantImage={pendingTenantImage}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
trialPeriodDays={trialPeriodDays}
|
trialPeriodDays={trialPeriodDays}
|
||||||
|
|||||||
142
app/javascript/components/Tour/Tour.tsx
Normal file
142
app/javascript/components/Tour/Tour.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import Joyride from 'react-joyride';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
userFullName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BOOTSTRAP_BREAKPOINT_SM = 768;
|
||||||
|
|
||||||
|
const Tour = ({ userFullName }: Props) => {
|
||||||
|
const boardsToggler = document.querySelector('button.navbarToggler') as HTMLElement;
|
||||||
|
const profileToggler = document.getElementById('navbarDropdown');
|
||||||
|
const userFirstName = userFullName ? userFullName.split(' ')[0].trim() : '';
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
target: 'body',
|
||||||
|
placement: 'center',
|
||||||
|
title: (userFirstName ? `${userFirstName}, w` : 'W') + 'elcome to your new feedback space!',
|
||||||
|
content: 'Learn how to use and customize your feedback space with a 30-second tour.',
|
||||||
|
disableBeacon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '.boardsNav',
|
||||||
|
title: 'Boards',
|
||||||
|
content: 'From the top navigation bar, you can access your roadmap and boards.',
|
||||||
|
disableBeacon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '.postListItem',
|
||||||
|
title: 'Feedback',
|
||||||
|
content: 'Each board contains feedback posted by your customers.',
|
||||||
|
disableBeacon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '.sidebarFilters',
|
||||||
|
placement: 'right-start',
|
||||||
|
title: 'Filters',
|
||||||
|
content: 'On the left sidebar, filters help you sort out and make sense of all received feedback.',
|
||||||
|
disableBeacon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '.siteSettingsDropdown',
|
||||||
|
title: 'Site settings',
|
||||||
|
content: 'Click "Site settings" to customize your feedback space. You can add custom boards and statuses, manage users, personalize appearance, and more.',
|
||||||
|
disableBeacon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: '.tourDropdown',
|
||||||
|
title: 'That\'s all!',
|
||||||
|
content: 'We hope Astuto will help you understand your customers and make better decisions! You can always replay this tour from here.',
|
||||||
|
disableBeacon: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const openBoardsNav = () => {
|
||||||
|
if (boardsToggler.getAttribute('aria-expanded') === 'false') {
|
||||||
|
boardsToggler.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeBoardsNav = () => {
|
||||||
|
if (boardsToggler.getAttribute('aria-expanded') === 'true') {
|
||||||
|
boardsToggler.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openProfileNav = () => {
|
||||||
|
if (profileToggler.getAttribute('aria-expanded') === 'false') {
|
||||||
|
profileToggler.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeProfileNav = () => {
|
||||||
|
if (profileToggler.getAttribute('aria-expanded') === 'true') {
|
||||||
|
profileToggler.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Joyride
|
||||||
|
steps={steps}
|
||||||
|
callback={state => {
|
||||||
|
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
|
||||||
|
|
||||||
|
// Open boards navbar (only on mobile)
|
||||||
|
if (
|
||||||
|
vw < BOOTSTRAP_BREAKPOINT_SM &&
|
||||||
|
state.type === 'step:after' &&
|
||||||
|
(((state.action === 'next' || state.action === 'close') && state.step.target === 'body') ||
|
||||||
|
(state.action === 'prev' && state.step.target === '.postListItem'))
|
||||||
|
) {
|
||||||
|
openBoardsNav();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close boards navbar (only on mobile)
|
||||||
|
if (
|
||||||
|
vw < BOOTSTRAP_BREAKPOINT_SM &&
|
||||||
|
state.type === 'step:after' &&
|
||||||
|
(//(state.action === 'next' && state.step.target === '.boardsNav') || // This causes positioniting problems for Joyride tour
|
||||||
|
(state.action === 'prev' && state.step.target === '.boardsNav'))
|
||||||
|
) {
|
||||||
|
closeBoardsNav();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open profile navbar
|
||||||
|
if (
|
||||||
|
state.type === 'step:after' &&
|
||||||
|
(((state.action === 'next' || state.action === 'close') && (state.step.target === '.sidebarFilters' || state.step.target === '.siteSettingsDropdown')) ||
|
||||||
|
(state.action === 'prev' && state.step.target === '.tourDropdown'))
|
||||||
|
) {
|
||||||
|
if (vw < BOOTSTRAP_BREAKPOINT_SM) openBoardsNav();
|
||||||
|
|
||||||
|
openProfileNav();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close everything on reset
|
||||||
|
if (state.action === 'reset') {
|
||||||
|
closeBoardsNav();
|
||||||
|
closeProfileNav();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
continuous
|
||||||
|
showSkipButton
|
||||||
|
disableScrolling
|
||||||
|
hideCloseButton
|
||||||
|
spotlightClicks={false}
|
||||||
|
locale={{
|
||||||
|
last: 'Finish',
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
overlay: { height: '200%' },
|
||||||
|
options: {
|
||||||
|
primaryColor: '#333333',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tour;
|
||||||
@@ -2,7 +2,8 @@ class User < ApplicationRecord
|
|||||||
include TenantOwnable
|
include TenantOwnable
|
||||||
|
|
||||||
devise :database_authenticatable, :registerable,
|
devise :database_authenticatable, :registerable,
|
||||||
:recoverable, :rememberable, :confirmable
|
:recoverable, :rememberable, :confirmable,
|
||||||
|
:trackable
|
||||||
|
|
||||||
validates_confirmation_of :password
|
validates_confirmation_of :password
|
||||||
|
|
||||||
|
|||||||
@@ -47,17 +47,18 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||||
<% if current_user.moderator? %>
|
<% if current_user.moderator? %>
|
||||||
|
<h6 class="dropdown-header"><%= t('header.menu.administration_header') %></h6>
|
||||||
<% if current_user.admin? or current_user.owner? %>
|
<% if current_user.admin? or current_user.owner? %>
|
||||||
<%=
|
<%=
|
||||||
link_to t('header.menu.site_settings'),
|
link_to t('header.menu.site_settings'),
|
||||||
@header_full_urls ? get_url_for(method(:site_settings_general_url)) : site_settings_general_path,
|
@header_full_urls ? get_url_for(method(:site_settings_general_url)) : site_settings_general_path,
|
||||||
class: 'dropdown-item'
|
class: 'dropdown-item siteSettingsDropdown'
|
||||||
%>
|
%>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%=
|
<%=
|
||||||
link_to t('header.menu.site_settings'),
|
link_to t('header.menu.site_settings'),
|
||||||
@header_full_urls ? get_url_for(method(:site_settings_users_url)) : site_settings_users_path,
|
@header_full_urls ? get_url_for(method(:site_settings_users_url)) : site_settings_users_path,
|
||||||
class: 'dropdown-item'
|
class: 'dropdown-item siteSettingsDropdown'
|
||||||
%>
|
%>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if current_user.owner? and Rails.application.multi_tenancy? %>
|
<% if current_user.owner? and Rails.application.multi_tenancy? %>
|
||||||
@@ -72,8 +73,20 @@
|
|||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<h6 class="dropdown-header"><%= t('header.menu.profile_header') %></h6>
|
||||||
<%= link_to t('header.menu.profile_settings'), @header_full_urls ? get_url_for(method(:edit_user_registration_url)) : edit_user_registration_path, class: 'dropdown-item' %>
|
<%= link_to t('header.menu.profile_settings'), @header_full_urls ? get_url_for(method(:edit_user_registration_url)) : edit_user_registration_path, class: 'dropdown-item' %>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
|
|
||||||
|
<% if current_user.admin? %>
|
||||||
|
<h6 class="dropdown-header"><%= t('header.menu.help_header') %></h6>
|
||||||
|
<% unless @boards.empty? %>
|
||||||
|
<%= link_to t('header.menu.tour'), @header_full_urls ? get_url_for(method(:board_url), resource: @boards.first, options: { tour: true }) : board_path(@boards.first, tour: true), class: 'dropdown-item tourDropdown' %>
|
||||||
|
<% end %>
|
||||||
|
<%= link_to t('header.menu.docs'), 'https://docs.astuto.io', class: 'dropdown-item', target: '_blank' %>
|
||||||
|
<%= link_to t('header.menu.support'), Rails.application.multi_tenancy? ? 'mailto:info@astuto.io' : 'https://github.com/astuto/astuto/issues', class: 'dropdown-item', target: '_blank' %>
|
||||||
|
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% unless @disable_sign_out %>
|
<% unless @disable_sign_out %>
|
||||||
<%= button_to t('header.menu.sign_out'), destroy_user_session_path, method: :delete, class: 'dropdown-item' %>
|
<%= button_to t('header.menu.sign_out'), destroy_user_session_path, method: :delete, class: 'dropdown-item' %>
|
||||||
|
|||||||
12
app/views/layouts/_promo_banner.html.erb
Normal file
12
app/views/layouts/_promo_banner.html.erb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="promoBanner">
|
||||||
|
<span>
|
||||||
|
Want a feedback space like this?
|
||||||
|
<a href="https://astuto.io?utm_campaign=promobanner&utm_source=feedback.astuto.io">
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
or
|
||||||
|
<a href="https://login.astuto.io/signup">
|
||||||
|
start your 7-day free trial now!
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -22,6 +22,10 @@
|
|||||||
<%= render 'layouts/no_active_subscription_banner' %>
|
<%= render 'layouts/no_active_subscription_banner' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if Rails.application.multi_tenancy? && request.subdomain == "feedback" %>
|
||||||
|
<%= render 'layouts/promo_banner' %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% if @tenant %>
|
<% if @tenant %>
|
||||||
<%= render 'layouts/header' %>
|
<%= render 'layouts/header' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -37,5 +41,9 @@
|
|||||||
<%= @tenant.tenant_setting.custom_css %>
|
<%= @tenant.tenant_setting.custom_css %>
|
||||||
</style>
|
</style>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if params[:tour] == 'true' && user_signed_in? && current_user.admin? %>
|
||||||
|
<%= react_component('Tour/Tour', { userFullName: current_user.full_name }) %>
|
||||||
|
<% end %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
oauthUserName: @user_name,
|
oauthUserName: @user_name,
|
||||||
baseUrl: Rails.application.base_url,
|
baseUrl: Rails.application.base_url,
|
||||||
astutoLogoImage: image_url("logo.png"),
|
astutoLogoImage: image_url("logo.png"),
|
||||||
|
feedbackSpaceCreatedImage: image_url("feedback-space-created.png"),
|
||||||
pendingTenantImage: image_url("pending-tenant.png"),
|
pendingTenantImage: image_url("pending-tenant.png"),
|
||||||
trialPeriodDays: (Rails.application.trial_period_days / 1.day).to_i,
|
trialPeriodDays: (Rails.application.trial_period_days / 1.day).to_i,
|
||||||
authenticityToken: form_authenticity_token
|
authenticityToken: form_authenticity_token
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ class CreateStripeCustomer
|
|||||||
customer = Stripe::Customer.create({
|
customer = Stripe::Customer.create({
|
||||||
email: owner.email,
|
email: owner.email,
|
||||||
name: owner.full_name,
|
name: owner.full_name,
|
||||||
|
metadata: {
|
||||||
|
site_name: tenant.site_name,
|
||||||
|
subdomain: tenant.subdomain,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
tb = TenantBilling.first_or_create
|
tb = TenantBilling.first_or_create
|
||||||
tb.update!(customer_id: customer.id)
|
tb.update!(customer_id: customer.id)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class CreateWelcomeEntitiesWorkflow
|
|||||||
# Create some Boards
|
# Create some Boards
|
||||||
feature_board = Board.create!(
|
feature_board = Board.create!(
|
||||||
name: 'Feature Requests',
|
name: 'Feature Requests',
|
||||||
description: 'This is a **board**! You can create as many as you want from **site settings** and their description can be *Markdown formatted*.',
|
description: 'This is a **board**! Go to Site settings > Boards to customise it or add more!',
|
||||||
order: 0
|
order: 0
|
||||||
)
|
)
|
||||||
bug_board = Board.create!(
|
bug_board = Board.create!(
|
||||||
@@ -43,7 +43,7 @@ class CreateWelcomeEntitiesWorkflow
|
|||||||
|
|
||||||
# Create some Posts
|
# Create some Posts
|
||||||
post1 = Post.create!(
|
post1 = Post.create!(
|
||||||
title: "Welcome #{owner.full_name}! This is an example feedback post, click to learn more!",
|
title: 'This is an example feedback post, click to learn more!',
|
||||||
description: 'Users can submit feedback by publishing posts like this. You can assign a **status** to each post: this one, for example, is marked as "Planned". Remember that you can customise post statuses from Site settings > Statuses',
|
description: 'Users can submit feedback by publishing posts like this. You can assign a **status** to each post: this one, for example, is marked as "Planned". Remember that you can customise post statuses from Site settings > Statuses',
|
||||||
board_id: feature_board.id,
|
board_id: feature_board.id,
|
||||||
user_id: owner.id,
|
user_id: owner.id,
|
||||||
@@ -57,7 +57,7 @@ class CreateWelcomeEntitiesWorkflow
|
|||||||
|
|
||||||
post2 = Post.create!(
|
post2 = Post.create!(
|
||||||
title: 'There are multiple boards',
|
title: 'There are multiple boards',
|
||||||
description: 'For now you have Feature Requests and Bug Reports, but you can add or remove as many as you want! Just go to Site settings > Boards!',
|
description: 'We created two boards for you, "Feature Requests" and "Bug Reports", but you can add or remove as many as you want! Just head to Site settings > Boards!',
|
||||||
board_id: bug_board.id,
|
board_id: bug_board.id,
|
||||||
user_id: owner.id
|
user_id: owner.id
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ en:
|
|||||||
no_status: 'No status'
|
no_status: 'No status'
|
||||||
loading: 'Loading...'
|
loading: 'Loading...'
|
||||||
confirmation: 'Are you sure?'
|
confirmation: 'Are you sure?'
|
||||||
|
confirmation_board_delete: 'Warning: if there are feedback posts inside this board, then ALL these posts will be deleted as well. This action cannot be undone.'
|
||||||
unsaved_changes: 'Unsaved changes will be lost if you leave the page.'
|
unsaved_changes: 'Unsaved changes will be lost if you leave the page.'
|
||||||
edited: 'Edited'
|
edited: 'Edited'
|
||||||
enabled: 'Enabled'
|
enabled: 'Enabled'
|
||||||
@@ -77,8 +78,14 @@ en:
|
|||||||
other: '%{count} days ago'
|
other: '%{count} days ago'
|
||||||
header:
|
header:
|
||||||
menu:
|
menu:
|
||||||
|
administration_header: 'Administration'
|
||||||
site_settings: 'Site settings'
|
site_settings: 'Site settings'
|
||||||
|
profile_header: 'Profile'
|
||||||
profile_settings: 'Profile settings'
|
profile_settings: 'Profile settings'
|
||||||
|
help_header: 'Help'
|
||||||
|
tour: 'Tour'
|
||||||
|
docs: 'Documentation'
|
||||||
|
support: 'Support'
|
||||||
sign_out: 'Sign out'
|
sign_out: 'Sign out'
|
||||||
log_in: 'Log in / Sign up'
|
log_in: 'Log in / Sign up'
|
||||||
roadmap:
|
roadmap:
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class AddDeviseTrackableColumnsToUsers < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :users, :sign_in_count, :integer, default: 0, null: false
|
||||||
|
add_column :users, :current_sign_in_at, :datetime
|
||||||
|
add_column :users, :last_sign_in_at, :datetime
|
||||||
|
add_column :users, :current_sign_in_ip, :string
|
||||||
|
add_column :users, :last_sign_in_ip, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2024_05_14_112836) do
|
ActiveRecord::Schema.define(version: 2024_05_21_124018) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -199,6 +199,11 @@ ActiveRecord::Schema.define(version: 2024_05_14_112836) do
|
|||||||
t.bigint "tenant_id", null: false
|
t.bigint "tenant_id", null: false
|
||||||
t.string "oauth_token"
|
t.string "oauth_token"
|
||||||
t.boolean "has_set_password", default: true, null: false
|
t.boolean "has_set_password", default: true, null: false
|
||||||
|
t.integer "sign_in_count", default: 0, null: false
|
||||||
|
t.datetime "current_sign_in_at"
|
||||||
|
t.datetime "last_sign_in_at"
|
||||||
|
t.string "current_sign_in_ip"
|
||||||
|
t.string "last_sign_in_ip"
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
t.index ["email", "tenant_id"], name: "index_users_on_email_and_tenant_id", unique: true
|
t.index ["email", "tenant_id"], name: "index_users_on_email_and_tenant_id", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"react-hook-form": "7.33.1",
|
"react-hook-form": "7.33.1",
|
||||||
"react-icons": "5.0.1",
|
"react-icons": "5.0.1",
|
||||||
"react-infinite-scroller": "1.2.4",
|
"react-infinite-scroller": "1.2.4",
|
||||||
|
"react-joyride": "2.8.1",
|
||||||
"react-markdown": "5.0.3",
|
"react-markdown": "5.0.3",
|
||||||
"react-redux": "7.1.1",
|
"react-redux": "7.1.1",
|
||||||
"react-sticky-box": "1.0.2",
|
"react-sticky-box": "1.0.2",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ FactoryBot.define do
|
|||||||
notifications_enabled { true }
|
notifications_enabled { true }
|
||||||
password { 'password' }
|
password { 'password' }
|
||||||
role { 'user' }
|
role { 'user' }
|
||||||
|
sign_in_count { 10 }
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :moderator, class: User do
|
factory :moderator, class: User do
|
||||||
@@ -14,6 +15,7 @@ FactoryBot.define do
|
|||||||
sequence(:full_name) { |n| "User Moderator #{n}" }
|
sequence(:full_name) { |n| "User Moderator #{n}" }
|
||||||
password { 'password' }
|
password { 'password' }
|
||||||
role { 'moderator' }
|
role { 'moderator' }
|
||||||
|
sign_in_count { 10 }
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :admin, class: User do
|
factory :admin, class: User do
|
||||||
@@ -22,6 +24,7 @@ FactoryBot.define do
|
|||||||
sequence(:full_name) { |n| "User Admin #{n}" }
|
sequence(:full_name) { |n| "User Admin #{n}" }
|
||||||
password { 'password' }
|
password { 'password' }
|
||||||
role { 'admin' }
|
role { 'admin' }
|
||||||
|
sign_in_count { 10 }
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :owner, class: User do
|
factory :owner, class: User do
|
||||||
@@ -30,6 +33,7 @@ FactoryBot.define do
|
|||||||
sequence(:full_name) { |n| "User Owner #{n}" }
|
sequence(:full_name) { |n| "User Owner #{n}" }
|
||||||
password { 'password' }
|
password { 'password' }
|
||||||
role { 'owner' }
|
role { 'owner' }
|
||||||
|
sign_in_count { 10 }
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :blocked, class: User do
|
factory :blocked, class: User do
|
||||||
@@ -38,6 +42,7 @@ FactoryBot.define do
|
|||||||
sequence(:full_name) { |n| "User Blocked #{n}" }
|
sequence(:full_name) { |n| "User Blocked #{n}" }
|
||||||
password { 'password' }
|
password { 'password' }
|
||||||
status { 'blocked' }
|
status { 'blocked' }
|
||||||
|
sign_in_count { 10 }
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :deleted, class: User do
|
factory :deleted, class: User do
|
||||||
@@ -46,5 +51,6 @@ FactoryBot.define do
|
|||||||
sequence(:full_name) { |n| "User Deleted #{n}" }
|
sequence(:full_name) { |n| "User Deleted #{n}" }
|
||||||
password { 'password' }
|
password { 'password' }
|
||||||
status { 'deleted' }
|
status { 'deleted' }
|
||||||
|
sign_in_count { 10 }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ feature 'site settings: boards', type: :system, js: true do
|
|||||||
find('.deleteAction').click
|
find('.deleteAction').click
|
||||||
|
|
||||||
alert = page.driver.browser.switch_to.alert
|
alert = page.driver.browser.switch_to.alert
|
||||||
expect(alert.text).to eq('Are you sure?')
|
expect(alert.text).to include('Are you sure?')
|
||||||
alert.accept
|
alert.accept
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
115
yarn.lock
115
yarn.lock
@@ -1103,6 +1103,16 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||||
|
|
||||||
|
"@gilbarbara/deep-equal@^0.1.1":
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@gilbarbara/deep-equal/-/deep-equal-0.1.2.tgz#1a106721368dba5e7e9fb7e9a3a6f9efbd8df36d"
|
||||||
|
integrity sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==
|
||||||
|
|
||||||
|
"@gilbarbara/deep-equal@^0.3.1":
|
||||||
|
version "0.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz#9c72ed0b2e6f8edb1580217e28d78b5b03ad4aee"
|
||||||
|
integrity sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==
|
||||||
|
|
||||||
"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
|
"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
|
||||||
version "0.3.3"
|
version "0.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
|
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
|
||||||
@@ -1685,6 +1695,16 @@ debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "2.1.2"
|
ms "2.1.2"
|
||||||
|
|
||||||
|
deep-diff@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26"
|
||||||
|
integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==
|
||||||
|
|
||||||
|
deepmerge@^4.3.1:
|
||||||
|
version "4.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||||
|
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||||
|
|
||||||
dom-serializer@^1.0.1:
|
dom-serializer@^1.0.1:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
|
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
|
||||||
@@ -2029,6 +2049,16 @@ is-hexadecimal@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
|
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
|
||||||
integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
|
integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
|
||||||
|
|
||||||
|
is-lite@^0.8.2:
|
||||||
|
version "0.8.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-lite/-/is-lite-0.8.2.tgz#26ab98b32aae8cc8b226593b9a641d2bf4bd3b6a"
|
||||||
|
integrity sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==
|
||||||
|
|
||||||
|
is-lite@^1.2.0, is-lite@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-lite/-/is-lite-1.2.1.tgz#401f30bfccd34cb8cc1283f958907c97859d8f25"
|
||||||
|
integrity sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==
|
||||||
|
|
||||||
is-number@^7.0.0:
|
is-number@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||||
@@ -2334,7 +2364,7 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^4.0.0"
|
find-up "^4.0.0"
|
||||||
|
|
||||||
popper.js@1.16.1:
|
popper.js@1.16.1, popper.js@^1.16.0:
|
||||||
version "1.16.1"
|
version "1.16.1"
|
||||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
|
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
|
||||||
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
|
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
|
||||||
@@ -2348,6 +2378,15 @@ prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.8.1"
|
react-is "^16.8.1"
|
||||||
|
|
||||||
|
prop-types@^15.8.1:
|
||||||
|
version "15.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
react-is "^16.13.1"
|
||||||
|
|
||||||
punycode@^2.1.0:
|
punycode@^2.1.0:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||||
@@ -2409,6 +2448,17 @@ react-dom@16.9.0:
|
|||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.15.0"
|
scheduler "^0.15.0"
|
||||||
|
|
||||||
|
react-floater@^0.7.9:
|
||||||
|
version "0.7.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-floater/-/react-floater-0.7.9.tgz#b15a652e817f200bfa42a2023ee8d3105803b968"
|
||||||
|
integrity sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==
|
||||||
|
dependencies:
|
||||||
|
deepmerge "^4.3.1"
|
||||||
|
is-lite "^0.8.2"
|
||||||
|
popper.js "^1.16.0"
|
||||||
|
prop-types "^15.8.1"
|
||||||
|
tree-changes "^0.9.1"
|
||||||
|
|
||||||
react-gravatar@2.6.3:
|
react-gravatar@2.6.3:
|
||||||
version "2.6.3"
|
version "2.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-gravatar/-/react-gravatar-2.6.3.tgz#5407eb6ac87e830e2a34deb760d2a4c404eb1dac"
|
resolved "https://registry.yarnpkg.com/react-gravatar/-/react-gravatar-2.6.3.tgz#5407eb6ac87e830e2a34deb760d2a4c404eb1dac"
|
||||||
@@ -2435,21 +2485,43 @@ react-infinite-scroller@1.2.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.5.8"
|
prop-types "^15.5.8"
|
||||||
|
|
||||||
|
react-innertext@^1.1.5:
|
||||||
|
version "1.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-innertext/-/react-innertext-1.1.5.tgz#8147ac54db3f7067d95f49e2d2c05a720d27d8d0"
|
||||||
|
integrity sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==
|
||||||
|
|
||||||
|
react-is@^16.13.1, react-is@^16.8.6:
|
||||||
|
version "16.13.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
|
react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
|
||||||
version "16.12.0"
|
version "16.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
||||||
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
|
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
|
||||||
|
|
||||||
react-is@^16.8.6:
|
|
||||||
version "16.13.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
|
||||||
|
|
||||||
react-is@^17.0.2:
|
react-is@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||||
|
|
||||||
|
react-joyride@2.8.1:
|
||||||
|
version "2.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-joyride/-/react-joyride-2.8.1.tgz#c87941885803a765dd870e1a3341bdc4a978cc80"
|
||||||
|
integrity sha512-fVwCmoOvJsiFKKHn8mvPUYc4JUUkgAsQMvarpZDtFPTc4duj240b12+AB8+3NXlTYGZVnKNSTgFFzoSh9RxjmQ==
|
||||||
|
dependencies:
|
||||||
|
"@gilbarbara/deep-equal" "^0.3.1"
|
||||||
|
deep-diff "^1.0.2"
|
||||||
|
deepmerge "^4.3.1"
|
||||||
|
is-lite "^1.2.1"
|
||||||
|
react-floater "^0.7.9"
|
||||||
|
react-innertext "^1.1.5"
|
||||||
|
react-is "^16.13.1"
|
||||||
|
scroll "^3.0.1"
|
||||||
|
scrollparent "^2.1.0"
|
||||||
|
tree-changes "^0.11.2"
|
||||||
|
type-fest "^4.15.0"
|
||||||
|
|
||||||
react-markdown@5.0.3:
|
react-markdown@5.0.3:
|
||||||
version "5.0.3"
|
version "5.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-5.0.3.tgz#41040ea7a9324b564b328fb81dd6c04f2a5373ac"
|
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-5.0.3.tgz#41040ea7a9324b564b328fb81dd6c04f2a5373ac"
|
||||||
@@ -2672,6 +2744,16 @@ schema-utils@^4.0.0:
|
|||||||
ajv-formats "^2.1.1"
|
ajv-formats "^2.1.1"
|
||||||
ajv-keywords "^5.1.0"
|
ajv-keywords "^5.1.0"
|
||||||
|
|
||||||
|
scroll@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/scroll/-/scroll-3.0.1.tgz#d5afb59fb3592ee3df31c89743e78b39e4cd8a26"
|
||||||
|
integrity sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==
|
||||||
|
|
||||||
|
scrollparent@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/scrollparent/-/scrollparent-2.1.0.tgz#6cae915c953835886a6ba0d77fdc2bb1ed09076d"
|
||||||
|
integrity sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==
|
||||||
|
|
||||||
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
|
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
|
||||||
version "6.3.1"
|
version "6.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
@@ -2793,6 +2875,22 @@ to-regex-range@^5.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number "^7.0.0"
|
is-number "^7.0.0"
|
||||||
|
|
||||||
|
tree-changes@^0.11.2:
|
||||||
|
version "0.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tree-changes/-/tree-changes-0.11.2.tgz#e02e65c4faae6230dfe357aa97a26e8eb7c7d321"
|
||||||
|
integrity sha512-4gXlUthrl+RabZw6lLvcCDl6KfJOCmrC16BC5CRdut1EAH509Omgg0BfKLY+ViRlzrvYOTWR0FMS2SQTwzumrw==
|
||||||
|
dependencies:
|
||||||
|
"@gilbarbara/deep-equal" "^0.3.1"
|
||||||
|
is-lite "^1.2.0"
|
||||||
|
|
||||||
|
tree-changes@^0.9.1:
|
||||||
|
version "0.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/tree-changes/-/tree-changes-0.9.3.tgz#89433ab3b4250c2910d386be1f83912b7144efcc"
|
||||||
|
integrity sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==
|
||||||
|
dependencies:
|
||||||
|
"@gilbarbara/deep-equal" "^0.1.1"
|
||||||
|
is-lite "^0.8.2"
|
||||||
|
|
||||||
trough@^1.0.0:
|
trough@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
|
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
|
||||||
@@ -2808,6 +2906,11 @@ turbolinks@5.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/turbolinks/-/turbolinks-5.2.0.tgz#e6877a55ea5c1cb3bb225f0a4ae303d6d32ff77c"
|
resolved "https://registry.yarnpkg.com/turbolinks/-/turbolinks-5.2.0.tgz#e6877a55ea5c1cb3bb225f0a4ae303d6d32ff77c"
|
||||||
integrity sha512-pMiez3tyBo6uRHFNNZoYMmrES/IaGgMhQQM+VFF36keryjb5ms0XkVpmKHkfW/4Vy96qiGW3K9bz0tF5sK9bBw==
|
integrity sha512-pMiez3tyBo6uRHFNNZoYMmrES/IaGgMhQQM+VFF36keryjb5ms0XkVpmKHkfW/4Vy96qiGW3K9bz0tF5sK9bBw==
|
||||||
|
|
||||||
|
type-fest@^4.15.0:
|
||||||
|
version "4.18.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.18.2.tgz#8d765c42e7280a11f4d04fb77a00dacc417c8b05"
|
||||||
|
integrity sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==
|
||||||
|
|
||||||
typescript@4.3.2:
|
typescript@4.3.2:
|
||||||
version "4.3.2"
|
version "4.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
|
||||||
|
|||||||
Reference in New Issue
Block a user