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:
Riccardo Graziosi
2024-05-21 19:10:18 +02:00
committed by GitHub
parent 57ecddb035
commit f0346a73ec
32 changed files with 473 additions and 55 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -96,4 +96,9 @@
color: white !important;
background-color: var(--primary-color);
}
.dropdown-header {
text-transform: uppercase;
color: var(--astuto-grey);
}
}

View File

@@ -315,4 +315,12 @@ body {
.alert-warning,
.text-center,
.m-0;
}
.promoBanner {
@extend
.alert,
.alert-secondary,
.text-center,
.m-0;
}

View File

@@ -33,6 +33,10 @@
}
}
}
.sidebarFilters {
@extend .m-0, .p-0;
}
.postStatusListItemContainer {
@extend

View File

@@ -37,7 +37,7 @@
.editUser {
display: block;
width: fit-content;
margin-top: 4px;
margin-top: 8px;
margin-left: auto;
margin-right: auto;
}

View File

@@ -1,7 +1,7 @@
:root {
// Theme palette (supposed to be customized)
--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)
--primary-color-light: color-mix(in srgb,var(--primary-color), #fff 85%);

View File

@@ -9,6 +9,15 @@ class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
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
def configure_permitted_parameters

View File

@@ -89,7 +89,7 @@ class OAuthsController < ApplicationController
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)
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}"
@page_title = "Create your feedback space"
render 'tenants/new'
else

View File

@@ -8,8 +8,9 @@ class RegistrationsController < Devise::RegistrationsController
# Override destroy to soft delete
def destroy
resource.status = "deleted"
resource.email = ''
resource.email = "#{SecureRandom.alphanumeric(16)}@deleted.com"
resource.full_name = t('defaults.deleted_user_full_name')
resource.skip_confirmation
resource.save
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
set_flash_message :notice, :destroyed

View File

@@ -112,33 +112,35 @@ class BoardP extends React.Component<Props> {
isLoggedIn={isLoggedIn}
authenticityToken={authenticityToken}
/>
<SearchFilter
searchQuery={filters.searchQuery}
handleChange={handleSearchFilterChange}
/>
{
isPowerUser &&
<>
<SortByFilter
sortBy={filters.sortBy}
handleChange={sortBy => handleSortByFilterChange(sortBy)}
<div className="sidebarFilters">
<SearchFilter
searchQuery={filters.searchQuery}
handleChange={handleSearchFilterChange}
/>
{
isPowerUser &&
<>
<SortByFilter
sortBy={filters.sortBy}
handleChange={sortBy => handleSortByFilterChange(sortBy)}
/>
<DateFilter
startDate={filters.date.startDate}
endDate={filters.date.endDate}
handleChange={handleDateFilterChange}
<DateFilter
startDate={filters.date.startDate}
endDate={filters.date.endDate}
handleChange={handleDateFilterChange}
/>
</>
}
<PostStatusFilter
postStatuses={postStatuses.items}
areLoading={postStatuses.areLoading}
error={postStatuses.error}
currentFilter={filters.postStatusIds}
handleFilterClick={handlePostStatusFilterChange}
/>
</>
}
<PostStatusFilter
postStatuses={postStatuses.items}
areLoading={postStatuses.areLoading}
error={postStatuses.error}
currentFilter={filters.postStatusIds}
handleFilterClick={handlePostStatusFilterChange}
/>
</div>
{ tenantSetting.show_powered_by && <PoweredByLink /> }
</Sidebar>

View File

@@ -97,7 +97,7 @@ class BoardsEditable extends React.Component<Props, State> {
</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 />}
customClass="deleteAction"
>

View File

@@ -7,7 +7,7 @@ interface Props {
pendingTenantImage: string;
}
const ConfirmSignUpPage = ({
const ConfirmEmailSignUpPage = ({
subdomain,
userEmail,
pendingTenantImage,
@@ -23,4 +23,4 @@ const ConfirmSignUpPage = ({
</Box>
);
export default ConfirmSignUpPage;
export default ConfirmEmailSignUpPage;

View File

@@ -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;

View File

@@ -15,6 +15,7 @@ interface Props {
handleSignUpSubmit(siteName: string, subdomain: string): void;
trialPeriodDays: number;
currentStep: number;
setCurrentStep(step: number): void;
}
const TenantSignUpForm = ({
@@ -23,6 +24,7 @@ const TenantSignUpForm = ({
handleSignUpSubmit,
trialPeriodDays,
currentStep,
setCurrentStep,
}: Props) => {
const { register, handleSubmit, formState: { errors } } = useForm<ITenantSignUpTenantForm>();
const onSubmit: SubmitHandler<ITenantSignUpTenantForm> = data => {

View File

@@ -1,11 +1,12 @@
import * as React from 'react';
import { useState } from 'react';
import HttpStatus from '../../constants/http_status';
import ConfirmSignUpPage from './ConfirmSignUpPage';
import TenantSignUpForm from './TenantSignUpForm';
import UserSignUpForm from './UserSignUpForm';
import ConfirmEmailSignUpPage from './ConfirmEmailSignUpPage';
import ConfirmOAuthSignUpPage from './ConfirmOAuthSignUpPage';
import { IOAuth } from '../../interfaces/IOAuth';
import HttpStatus from '../../constants/http_status';
interface Props {
oAuthLoginCompleted: boolean;
@@ -27,6 +28,7 @@ interface Props {
): Promise<any>;
astutoLogoImage: string;
feedbackSpaceCreatedImage: string;
pendingTenantImage: string;
baseUrl: string;
@@ -58,6 +60,7 @@ const TenantSignUpP = ({
error,
handleSubmit,
astutoLogoImage,
feedbackSpaceCreatedImage,
pendingTenantImage,
baseUrl,
trialPeriodDays,
@@ -66,6 +69,9 @@ const TenantSignUpP = ({
// authMethod is either 'none', 'email' or 'oauth'
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({
fullName: oAuthLoginCompleted ? oauthUserName : '',
email: oAuthLoginCompleted ? oauthUserEmail : '',
@@ -91,15 +97,22 @@ const TenantSignUpP = ({
authenticityToken,
).then(res => {
if (res?.status !== HttpStatus.Created) return;
setTenantData({ siteName, subdomain });
setCurrentStep(currentStep + 1);
if (authMethod == 'oauth') {
let redirectUrl = new URL(baseUrl);
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;
}
setTenantData({ siteName, subdomain });
setCurrentStep(currentStep + 1);
});
}
@@ -118,23 +131,34 @@ const TenantSignUpP = ({
oAuths={oAuths}
userData={userData}
setUserData={setUserData}
setGoneBack={setGoneBack}
/>
}
{
(currentStep === 1 || currentStep === 2) &&
(goneBack || currentStep === 2) &&
<TenantSignUpForm
isSubmitting={isSubmitting}
error={error}
handleSignUpSubmit={handleSignUpSubmit}
trialPeriodDays={trialPeriodDays}
currentStep={currentStep}
setCurrentStep={setCurrentStep}
/>
}
{
currentStep === 3 &&
<ConfirmSignUpPage
currentStep === 3 && authMethod === 'oauth' &&
<ConfirmOAuthSignUpPage
baseUrl={baseUrl}
subdomain={tenantData.subdomain}
feedbackSpaceCreatedImage={feedbackSpaceCreatedImage}
/>
}
{
currentStep === 3 && authMethod === 'email' &&
<ConfirmEmailSignUpPage
subdomain={tenantData.subdomain}
userEmail={userData.email}
pendingTenantImage={pendingTenantImage}

View File

@@ -21,6 +21,7 @@ interface Props {
oAuths: Array<IOAuth>;
userData: ITenantSignUpUserForm;
setUserData({}: ITenantSignUpUserForm): void;
setGoneBack(goneBack: boolean): void;
}
const UserSignUpForm = ({
@@ -31,6 +32,7 @@ const UserSignUpForm = ({
oAuths,
userData,
setUserData,
setGoneBack,
}: Props) => {
const {
register,
@@ -164,7 +166,16 @@ const UserSignUpForm = ({
currentStep === 2 &&
<p className="userRecap">
<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>
}
</Box>

View File

@@ -14,6 +14,7 @@ interface Props {
oauthUserName?: string;
baseUrl: string;
astutoLogoImage: string;
feedbackSpaceCreatedImage: string;
pendingTenantImage: string;
trialPeriodDays: number;
authenticityToken: string;
@@ -35,6 +36,7 @@ class TenantSignUpRoot extends React.Component<Props> {
oauthUserEmail,
oauthUserName,
astutoLogoImage,
feedbackSpaceCreatedImage,
pendingTenantImage,
baseUrl,
trialPeriodDays,
@@ -49,6 +51,7 @@ class TenantSignUpRoot extends React.Component<Props> {
oauthUserName={oauthUserName}
oAuths={oAuths.map(oAuth => oAuthJSON2JS(oAuth))}
astutoLogoImage={astutoLogoImage}
feedbackSpaceCreatedImage={feedbackSpaceCreatedImage}
pendingTenantImage={pendingTenantImage}
baseUrl={baseUrl}
trialPeriodDays={trialPeriodDays}

View 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;

View File

@@ -2,7 +2,8 @@ class User < ApplicationRecord
include TenantOwnable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :confirmable
:recoverable, :rememberable, :confirmable,
:trackable
validates_confirmation_of :password

View File

@@ -47,17 +47,18 @@
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<% if current_user.moderator? %>
<h6 class="dropdown-header"><%= t('header.menu.administration_header') %></h6>
<% if current_user.admin? or current_user.owner? %>
<%=
link_to t('header.menu.site_settings'),
@header_full_urls ? get_url_for(method(:site_settings_general_url)) : site_settings_general_path,
class: 'dropdown-item'
class: 'dropdown-item siteSettingsDropdown'
%>
<% else %>
<%=
link_to t('header.menu.site_settings'),
@header_full_urls ? get_url_for(method(:site_settings_users_url)) : site_settings_users_path,
class: 'dropdown-item'
class: 'dropdown-item siteSettingsDropdown'
%>
<% end %>
<% if current_user.owner? and Rails.application.multi_tenancy? %>
@@ -72,8 +73,20 @@
<div class="dropdown-divider"></div>
<% 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' %>
<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 %>
<%= button_to t('header.menu.sign_out'), destroy_user_session_path, method: :delete, class: 'dropdown-item' %>

View 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>

View File

@@ -22,6 +22,10 @@
<%= render 'layouts/no_active_subscription_banner' %>
<% end %>
<% if Rails.application.multi_tenancy? && request.subdomain == "feedback" %>
<%= render 'layouts/promo_banner' %>
<% end %>
<% if @tenant %>
<%= render 'layouts/header' %>
<% end %>
@@ -37,5 +41,9 @@
<%= @tenant.tenant_setting.custom_css %>
</style>
<% end %>
<% if params[:tour] == 'true' && user_signed_in? && current_user.admin? %>
<%= react_component('Tour/Tour', { userFullName: current_user.full_name }) %>
<% end %>
</body>
</html>

View File

@@ -8,6 +8,7 @@
oauthUserName: @user_name,
baseUrl: Rails.application.base_url,
astutoLogoImage: image_url("logo.png"),
feedbackSpaceCreatedImage: image_url("feedback-space-created.png"),
pendingTenantImage: image_url("pending-tenant.png"),
trialPeriodDays: (Rails.application.trial_period_days / 1.day).to_i,
authenticityToken: form_authenticity_token

View File

@@ -8,6 +8,10 @@ class CreateStripeCustomer
customer = Stripe::Customer.create({
email: owner.email,
name: owner.full_name,
metadata: {
site_name: tenant.site_name,
subdomain: tenant.subdomain,
},
})
tb = TenantBilling.first_or_create
tb.update!(customer_id: customer.id)

View File

@@ -6,7 +6,7 @@ class CreateWelcomeEntitiesWorkflow
# Create some Boards
feature_board = Board.create!(
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
)
bug_board = Board.create!(
@@ -43,7 +43,7 @@ class CreateWelcomeEntitiesWorkflow
# Create some Posts
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',
board_id: feature_board.id,
user_id: owner.id,
@@ -57,7 +57,7 @@ class CreateWelcomeEntitiesWorkflow
post2 = Post.create!(
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,
user_id: owner.id
)

View File

@@ -46,6 +46,7 @@ en:
no_status: 'No status'
loading: 'Loading...'
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.'
edited: 'Edited'
enabled: 'Enabled'
@@ -77,8 +78,14 @@ en:
other: '%{count} days ago'
header:
menu:
administration_header: 'Administration'
site_settings: 'Site settings'
profile_header: 'Profile'
profile_settings: 'Profile settings'
help_header: 'Help'
tour: 'Tour'
docs: 'Documentation'
support: 'Support'
sign_out: 'Sign out'
log_in: 'Log in / Sign up'
roadmap:

View File

@@ -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

View File

@@ -10,7 +10,7 @@
#
# 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
enable_extension "plpgsql"
@@ -199,6 +199,11 @@ ActiveRecord::Schema.define(version: 2024_05_14_112836) do
t.bigint "tenant_id", null: false
t.string "oauth_token"
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 ["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

View File

@@ -35,6 +35,7 @@
"react-hook-form": "7.33.1",
"react-icons": "5.0.1",
"react-infinite-scroller": "1.2.4",
"react-joyride": "2.8.1",
"react-markdown": "5.0.3",
"react-redux": "7.1.1",
"react-sticky-box": "1.0.2",

View File

@@ -6,6 +6,7 @@ FactoryBot.define do
notifications_enabled { true }
password { 'password' }
role { 'user' }
sign_in_count { 10 }
end
factory :moderator, class: User do
@@ -14,6 +15,7 @@ FactoryBot.define do
sequence(:full_name) { |n| "User Moderator #{n}" }
password { 'password' }
role { 'moderator' }
sign_in_count { 10 }
end
factory :admin, class: User do
@@ -22,6 +24,7 @@ FactoryBot.define do
sequence(:full_name) { |n| "User Admin #{n}" }
password { 'password' }
role { 'admin' }
sign_in_count { 10 }
end
factory :owner, class: User do
@@ -30,6 +33,7 @@ FactoryBot.define do
sequence(:full_name) { |n| "User Owner #{n}" }
password { 'password' }
role { 'owner' }
sign_in_count { 10 }
end
factory :blocked, class: User do
@@ -38,6 +42,7 @@ FactoryBot.define do
sequence(:full_name) { |n| "User Blocked #{n}" }
password { 'password' }
status { 'blocked' }
sign_in_count { 10 }
end
factory :deleted, class: User do
@@ -46,5 +51,6 @@ FactoryBot.define do
sequence(:full_name) { |n| "User Deleted #{n}" }
password { 'password' }
status { 'deleted' }
sign_in_count { 10 }
end
end

View File

@@ -103,7 +103,7 @@ feature 'site settings: boards', type: :system, js: true do
find('.deleteAction').click
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
end

115
yarn.lock
View File

@@ -1103,6 +1103,16 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
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":
version "0.3.3"
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:
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:
version "1.4.1"
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"
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:
version "7.0.0"
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:
find-up "^4.0.0"
popper.js@1.16.1:
popper.js@1.16.1, popper.js@^1.16.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
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"
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:
version "2.1.1"
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"
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:
version "2.6.3"
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:
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:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
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:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
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:
version "5.0.3"
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-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:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
@@ -2793,6 +2875,22 @@ to-regex-range@^5.0.1:
dependencies:
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:
version "1.0.5"
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"
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:
version "4.3.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"