mirror of
https://github.com/astuto/astuto.git
synced 2025-12-14 18:57:51 +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;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
text-transform: uppercase;
|
||||
color: var(--astuto-grey);
|
||||
}
|
||||
}
|
||||
@@ -315,4 +315,12 @@ body {
|
||||
.alert-warning,
|
||||
.text-center,
|
||||
.m-0;
|
||||
}
|
||||
|
||||
.promoBanner {
|
||||
@extend
|
||||
.alert,
|
||||
.alert-secondary,
|
||||
.text-center,
|
||||
.m-0;
|
||||
}
|
||||
@@ -33,6 +33,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarFilters {
|
||||
@extend .m-0, .p-0;
|
||||
}
|
||||
|
||||
.postStatusListItemContainer {
|
||||
@extend
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
.editUser {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin-top: 4px;
|
||||
margin-top: 8px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@@ -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%);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
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 => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
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
|
||||
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :confirmable
|
||||
:recoverable, :rememberable, :confirmable,
|
||||
:trackable
|
||||
|
||||
validates_confirmation_of :password
|
||||
|
||||
|
||||
@@ -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' %>
|
||||
|
||||
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' %>
|
||||
<% 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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
115
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user