mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27:52 +01:00
Add Site settings > General (#133)
This commit is contained in:
committed by
GitHub
parent
bdc4004e4a
commit
35831b9801
@@ -9,8 +9,4 @@ SECRET_KEY_BASE=secretkeybasehere
|
|||||||
POSTGRES_USER=yourusernamehere
|
POSTGRES_USER=yourusernamehere
|
||||||
POSTGRES_PASSWORD=yourpasswordhere
|
POSTGRES_PASSWORD=yourpasswordhere
|
||||||
|
|
||||||
APP_NAME=You App Name Here
|
EMAIL_CONFIRMATION=false
|
||||||
SHOW_LOGO=yes
|
|
||||||
POSTS_PER_PAGE=15
|
|
||||||
|
|
||||||
EMAIL_CONFIRMATION=no
|
|
||||||
@@ -65,6 +65,10 @@ The project is broadly structured as follows:
|
|||||||
- `schema.rb`: database schema
|
- `schema.rb`: database schema
|
||||||
- `spec`: RSpec tests
|
- `spec`: RSpec tests
|
||||||
|
|
||||||
|
### Rails console
|
||||||
|
|
||||||
|
If you need to work with the Rails console, just attach a shell to the `web` container. From there, type `rails c` to run the console. You may notice that every query you run (e.g. `Post.all`) fails with error `Current::MissingCurrentTenant (Current tenant is not set)`: that's because Astuto implements multi tenancy at the database level. In order to fix this error, supposing you're in single tenant mode, just run `Current.tenant = Tenant.first` as the first command inside the Rails console. After that, everything should work as expected.
|
||||||
|
|
||||||
### Specs (tests)
|
### Specs (tests)
|
||||||
|
|
||||||
Tests are done using RSpec, a testing framework for Ruby on Rails:
|
Tests are done using RSpec, a testing framework for Ruby on Rails:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
|
|||||||
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
||||||
|
|
||||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||||
before_action :load_boards
|
prepend_before_action :load_tenant_data
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
@@ -15,8 +15,36 @@ class ApplicationController < ActionController::Base
|
|||||||
devise_parameter_sanitizer.permit(:account_update, keys: additional_permitted_parameters)
|
devise_parameter_sanitizer.permit(:account_update, keys: additional_permitted_parameters)
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_boards
|
def load_tenant_data
|
||||||
|
if Rails.application.multi_tenancy?
|
||||||
|
return if request.subdomain.blank? or RESERVED_SUBDOMAINS.include?(request.subdomain)
|
||||||
|
|
||||||
|
# Load the current tenant based on subdomain
|
||||||
|
current_tenant = Tenant.find_by(subdomain: request.subdomain)
|
||||||
|
|
||||||
|
if current_tenant.status == "pending" and controller_name != "confirmation" and action_name != "show"
|
||||||
|
redirect_to pending_tenant_path; return
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_tenant.status == "blocked"
|
||||||
|
redirect_to blocked_tenant_path; return
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to showcase_url unless current_tenant
|
||||||
|
else
|
||||||
|
# Load the one and only tenant
|
||||||
|
current_tenant = Tenant.first
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless current_tenant
|
||||||
|
Current.tenant = current_tenant
|
||||||
|
|
||||||
|
# Load tenant data
|
||||||
|
@tenant = Current.tenant_or_raise!
|
||||||
@boards = Board.select(:id, :name).order(order: :asc)
|
@boards = Board.select(:id, :name).order(order: :asc)
|
||||||
|
|
||||||
|
# Setup locale
|
||||||
|
I18n.locale = @tenant.locale
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
class RegistrationsController < Devise::RegistrationsController
|
class RegistrationsController < Devise::RegistrationsController
|
||||||
|
# Needed to have Current.tenant available in Devise's controllers
|
||||||
|
prepend_before_action :load_tenant_data
|
||||||
|
|
||||||
# Override destroy to soft delete
|
# Override destroy to soft delete
|
||||||
def destroy
|
def destroy
|
||||||
resource.status = "deleted"
|
resource.status = "deleted"
|
||||||
|
|||||||
4
app/controllers/sessions_controller.rb
Normal file
4
app/controllers/sessions_controller.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
class SessionsController < Devise::SessionsController
|
||||||
|
# Needed to have Current.tenant available in Devise's controllers
|
||||||
|
prepend_before_action :load_tenant_data
|
||||||
|
end
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
class StaticPagesController < ApplicationController
|
class StaticPagesController < ApplicationController
|
||||||
|
skip_before_action :load_tenant_data, only: [:showcase, :pending_tenant, :blocked_tenant]
|
||||||
|
|
||||||
def roadmap
|
def roadmap
|
||||||
@post_statuses = PostStatus
|
@post_statuses = PostStatus
|
||||||
.find_roadmap
|
.find_roadmap
|
||||||
@@ -8,4 +10,14 @@ class StaticPagesController < ApplicationController
|
|||||||
.find_with_post_status_in(@post_statuses)
|
.find_with_post_status_in(@post_statuses)
|
||||||
.select(:id, :title, :board_id, :post_status_id, :user_id, :created_at)
|
.select(:id, :title, :board_id, :post_status_id, :user_id, :created_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def showcase
|
||||||
|
render html: 'Showcase home page.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def pending_tenant
|
||||||
|
end
|
||||||
|
|
||||||
|
def blocked_tenant
|
||||||
|
end
|
||||||
end
|
end
|
||||||
63
app/controllers/tenants_controller.rb
Normal file
63
app/controllers/tenants_controller.rb
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
class TenantsController < ApplicationController
|
||||||
|
include ApplicationHelper
|
||||||
|
|
||||||
|
before_action :authenticate_admin, only: [:show, :update]
|
||||||
|
|
||||||
|
def new
|
||||||
|
@page_title = t('signup.page_title')
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: Current.tenant_or_raise!
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@tenant = Tenant.new
|
||||||
|
@tenant.assign_attributes(tenant_create_params)
|
||||||
|
authorize @tenant
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@tenant.save!
|
||||||
|
Current.tenant = @tenant
|
||||||
|
|
||||||
|
@user = User.create!(
|
||||||
|
full_name: params[:user][:full_name],
|
||||||
|
email: params[:user][:email],
|
||||||
|
password: params[:user][:password],
|
||||||
|
role: "admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: @tenant, status: :created
|
||||||
|
|
||||||
|
rescue ActiveRecord::RecordInvalid => exception
|
||||||
|
render json: { error: exception }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@tenant = Current.tenant_or_raise!
|
||||||
|
authorize @tenant
|
||||||
|
|
||||||
|
if @tenant.update(tenant_update_params)
|
||||||
|
render json: @tenant
|
||||||
|
else
|
||||||
|
render json: {
|
||||||
|
error: @tenant.errors.full_messages
|
||||||
|
}, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def tenant_create_params
|
||||||
|
params
|
||||||
|
.require(:tenant)
|
||||||
|
.permit(policy(@tenant).permitted_attributes_for_create)
|
||||||
|
end
|
||||||
|
|
||||||
|
def tenant_update_params
|
||||||
|
params
|
||||||
|
.require(:tenant)
|
||||||
|
.permit(policy(@tenant).permitted_attributes_for_update)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -27,4 +27,10 @@ module ApplicationHelper
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_subdomain_to(url_helper, resource=nil, options={})
|
||||||
|
options[:subdomain] = Current.tenant_or_raise!.subdomain if Rails.application.multi_tenancy?
|
||||||
|
|
||||||
|
resource ? url_helper.call(resource, options) : url_helper.call(options)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
59
app/javascript/actions/Tenant/requestTenant.ts
Normal file
59
app/javascript/actions/Tenant/requestTenant.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Action } from 'redux';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
|
import ITenantJSON from '../../interfaces/json/ITenant';
|
||||||
|
import { State } from '../../reducers/rootReducer';
|
||||||
|
|
||||||
|
export const TENANT_REQUEST_START = 'TENANT_REQUEST_START';
|
||||||
|
interface TenantRequestStartAction {
|
||||||
|
type: typeof TENANT_REQUEST_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TENANT_REQUEST_SUCCESS = 'TENANT_REQUEST_SUCCESS';
|
||||||
|
interface TenantRequestSuccessAction {
|
||||||
|
type: typeof TENANT_REQUEST_SUCCESS;
|
||||||
|
tenant: ITenantJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TENANT_REQUEST_FAILURE = 'TENANT_REQUEST_FAILURE';
|
||||||
|
interface TenantRequestFailureAction {
|
||||||
|
type: typeof TENANT_REQUEST_FAILURE;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TenantRequestActionTypes =
|
||||||
|
TenantRequestStartAction |
|
||||||
|
TenantRequestSuccessAction |
|
||||||
|
TenantRequestFailureAction;
|
||||||
|
|
||||||
|
|
||||||
|
const tenantRequestStart = (): TenantRequestActionTypes => ({
|
||||||
|
type: TENANT_REQUEST_START,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tenantRequestSuccess = (
|
||||||
|
tenant: ITenantJSON
|
||||||
|
): TenantRequestActionTypes => ({
|
||||||
|
type: TENANT_REQUEST_SUCCESS,
|
||||||
|
tenant,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tenantRequestFailure = (error: string): TenantRequestActionTypes => ({
|
||||||
|
type: TENANT_REQUEST_FAILURE,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const requestTenant = (): ThunkAction<void, State, null, Action<string>> => (
|
||||||
|
async (dispatch) => {
|
||||||
|
dispatch(tenantRequestStart());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/tenants/0');
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
dispatch(tenantRequestSuccess(json));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(tenantRequestFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
82
app/javascript/actions/Tenant/submitTenant.ts
Normal file
82
app/javascript/actions/Tenant/submitTenant.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Action } from "redux";
|
||||||
|
import { ThunkAction } from "redux-thunk";
|
||||||
|
import HttpStatus from "../../constants/http_status";
|
||||||
|
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||||
|
import ITenantJSON from "../../interfaces/json/ITenant";
|
||||||
|
import { State } from "../../reducers/rootReducer";
|
||||||
|
|
||||||
|
export const TENANT_SUBMIT_START = 'TENANT_SUBMIT_START';
|
||||||
|
interface TenantSubmitStartAction {
|
||||||
|
type: typeof TENANT_SUBMIT_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TENANT_SUBMIT_SUCCESS = 'TENANT_SUBMIT_SUCCESS';
|
||||||
|
interface TenantSubmitSuccessAction {
|
||||||
|
type: typeof TENANT_SUBMIT_SUCCESS;
|
||||||
|
tenant: ITenantJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TENANT_SUBMIT_FAILURE = 'TENANT_SUBMIT_FAILURE';
|
||||||
|
interface TenantSubmitFailureAction {
|
||||||
|
type: typeof TENANT_SUBMIT_FAILURE;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TenantSubmitActionTypes =
|
||||||
|
TenantSubmitStartAction |
|
||||||
|
TenantSubmitSuccessAction |
|
||||||
|
TenantSubmitFailureAction;
|
||||||
|
|
||||||
|
const tenantSubmitStart = (): TenantSubmitStartAction => ({
|
||||||
|
type: TENANT_SUBMIT_START,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tenantSubmitSuccess = (
|
||||||
|
tenantJSON: ITenantJSON,
|
||||||
|
): TenantSubmitSuccessAction => ({
|
||||||
|
type: TENANT_SUBMIT_SUCCESS,
|
||||||
|
tenant: tenantJSON,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tenantSubmitFailure = (error: string): TenantSubmitFailureAction => ({
|
||||||
|
type: TENANT_SUBMIT_FAILURE,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const submitTenant = (
|
||||||
|
userFullName: string,
|
||||||
|
userEmail: string,
|
||||||
|
userPassword: string,
|
||||||
|
siteName: string,
|
||||||
|
subdomain: string,
|
||||||
|
authenticityToken: string,
|
||||||
|
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
|
dispatch(tenantSubmitStart());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/tenants`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: buildRequestHeaders(authenticityToken),
|
||||||
|
body: JSON.stringify({
|
||||||
|
user: {
|
||||||
|
full_name: userFullName,
|
||||||
|
email: userEmail,
|
||||||
|
password: userPassword,
|
||||||
|
},
|
||||||
|
tenant: {
|
||||||
|
site_name: siteName,
|
||||||
|
subdomain: subdomain,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (res.status === HttpStatus.Created) {
|
||||||
|
dispatch(tenantSubmitSuccess(json));
|
||||||
|
} else {
|
||||||
|
dispatch(tenantSubmitFailure(json.error));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(tenantSubmitFailure(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
124
app/javascript/actions/Tenant/tenantSignUpFormActions.ts
Normal file
124
app/javascript/actions/Tenant/tenantSignUpFormActions.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
export const TENANT_SIGN_UP_TOGGLE_EMAIL_AUTH = 'TENANT_SIGN_UP_TOGGLE_EMAIL_AUTH';
|
||||||
|
|
||||||
|
interface TenantSignUpToggleEmailAuth {
|
||||||
|
type: typeof TENANT_SIGN_UP_TOGGLE_EMAIL_AUTH,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleEmailAuthTenantSignUp = (
|
||||||
|
|
||||||
|
): TenantSignUpToggleEmailAuth => ({
|
||||||
|
type: TENANT_SIGN_UP_TOGGLE_EMAIL_AUTH,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// User full name
|
||||||
|
export const TENANT_SIGN_UP_CHANGE_USER_FULL_NAME = 'TENANT_SIGN_UP_CHANGE_USER_FULL_NAME';
|
||||||
|
|
||||||
|
interface TenantSignUpChangeUserFullName {
|
||||||
|
type: typeof TENANT_SIGN_UP_CHANGE_USER_FULL_NAME,
|
||||||
|
fullName: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeUserFullNameTenantSignUp = (
|
||||||
|
fullName: string
|
||||||
|
): TenantSignUpChangeUserFullName => ({
|
||||||
|
type: TENANT_SIGN_UP_CHANGE_USER_FULL_NAME,
|
||||||
|
fullName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// User email
|
||||||
|
export const TENANT_SIGN_UP_CHANGE_USER_EMAIL = 'TENANT_SIGN_UP_CHANGE_USER_EMAIL';
|
||||||
|
|
||||||
|
interface TenantSignUpChangeUserEmail {
|
||||||
|
type: typeof TENANT_SIGN_UP_CHANGE_USER_EMAIL,
|
||||||
|
email: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeUserEmailTenantSignUp = (
|
||||||
|
email: string
|
||||||
|
): TenantSignUpChangeUserEmail => ({
|
||||||
|
type: TENANT_SIGN_UP_CHANGE_USER_EMAIL,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
// User password
|
||||||
|
export const TENANT_SIGN_UP_CHANGE_USER_PASSWORD = 'TENANT_SIGN_UP_CHANGE_USER_PASSWORD';
|
||||||
|
|
||||||
|
interface TenantSignUpChangeUserPassword {
|
||||||
|
type: typeof TENANT_SIGN_UP_CHANGE_USER_PASSWORD,
|
||||||
|
password: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeUserPasswordTenantSignUp = (
|
||||||
|
password: string
|
||||||
|
): TenantSignUpChangeUserPassword => ({
|
||||||
|
type: TENANT_SIGN_UP_CHANGE_USER_PASSWORD,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
// User password confirmation
|
||||||
|
export const TENANT_SIGN_UP_CHANGE_USER_PASSWORD_CONFIRMATION = 'TENANT_SIGN_UP_CHANGE_USER_PASSWORD_CONFIRMATION';
|
||||||
|
|
||||||
|
interface TenantSignUpChangeUserPasswordConfirmation {
|
||||||
|
type: typeof TENANT_SIGN_UP_CHANGE_USER_PASSWORD_CONFIRMATION,
|
||||||
|
passwordConfirmation: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeUserPasswordConfirmationTenantSignUp = (
|
||||||
|
passwordConfirmation: string
|
||||||
|
): TenantSignUpChangeUserPasswordConfirmation => ({
|
||||||
|
type: TENANT_SIGN_UP_CHANGE_USER_PASSWORD_CONFIRMATION,
|
||||||
|
passwordConfirmation,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm user data, proceed to step 2
|
||||||
|
export const TENANT_SIGN_UP_CONFIRM_USER_FORM = 'TENANT_SIGN_UP_CONFIRM_USER_FORM';
|
||||||
|
|
||||||
|
interface TenantSignUpConfirmUserForm {
|
||||||
|
type: typeof TENANT_SIGN_UP_CONFIRM_USER_FORM;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const confirmUserFormTenantSignUp = (): TenantSignUpConfirmUserForm => ({
|
||||||
|
type: TENANT_SIGN_UP_CONFIRM_USER_FORM,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tenant site name
|
||||||
|
export const TENANT_SIGN_UP_CHANGE_TENANT_SITE_NAME = 'TENANT_SIGN_UP_CHANGE_TENANT_SITE_NAME';
|
||||||
|
|
||||||
|
interface TenantSignUpChangeTenantSiteName {
|
||||||
|
type: typeof TENANT_SIGN_UP_CHANGE_TENANT_SITE_NAME,
|
||||||
|
siteName: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeTenantSiteNameTenantSignUp = (
|
||||||
|
siteName: string
|
||||||
|
): TenantSignUpChangeTenantSiteName => ({
|
||||||
|
type: TENANT_SIGN_UP_CHANGE_TENANT_SITE_NAME,
|
||||||
|
siteName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tenant site name
|
||||||
|
export const TENANT_SIGN_UP_CHANGE_TENANT_SUBDOMAIN = 'TENANT_SIGN_UP_CHANGE_TENANT_SUBDOMAIN';
|
||||||
|
|
||||||
|
interface TenantSignUpChangeTenantSubdomain {
|
||||||
|
type: typeof TENANT_SIGN_UP_CHANGE_TENANT_SUBDOMAIN,
|
||||||
|
subdomain: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeTenantSubdomainTenantSignUp = (
|
||||||
|
subdomain: string
|
||||||
|
): TenantSignUpChangeTenantSubdomain => ({
|
||||||
|
type: TENANT_SIGN_UP_CHANGE_TENANT_SUBDOMAIN,
|
||||||
|
subdomain,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export type TenantSignUpFormActions =
|
||||||
|
TenantSignUpToggleEmailAuth |
|
||||||
|
TenantSignUpChangeUserFullName |
|
||||||
|
TenantSignUpChangeUserEmail |
|
||||||
|
TenantSignUpChangeUserPassword |
|
||||||
|
TenantSignUpChangeUserPasswordConfirmation |
|
||||||
|
TenantSignUpConfirmUserForm |
|
||||||
|
TenantSignUpChangeTenantSiteName |
|
||||||
|
TenantSignUpChangeTenantSubdomain;
|
||||||
91
app/javascript/actions/Tenant/updateTenant.ts
Normal file
91
app/javascript/actions/Tenant/updateTenant.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Action } from "redux";
|
||||||
|
import { ThunkAction } from "redux-thunk";
|
||||||
|
|
||||||
|
import HttpStatus from "../../constants/http_status";
|
||||||
|
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||||
|
import ITenantJSON from "../../interfaces/json/ITenant";
|
||||||
|
import { State } from "../../reducers/rootReducer";
|
||||||
|
|
||||||
|
export const TENANT_UPDATE_START = 'TENANT_UPDATE_START';
|
||||||
|
interface TenantUpdateStartAction {
|
||||||
|
type: typeof TENANT_UPDATE_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TENANT_UPDATE_SUCCESS = 'TENANT_UPDATE_SUCCESS';
|
||||||
|
interface TenantUpdateSuccessAction {
|
||||||
|
type: typeof TENANT_UPDATE_SUCCESS;
|
||||||
|
tenant: ITenantJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TENANT_UPDATE_FAILURE = 'TENANT_UPDATE_FAILURE';
|
||||||
|
interface TenantUpdateFailureAction {
|
||||||
|
type: typeof TENANT_UPDATE_FAILURE;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TenantUpdateActionTypes =
|
||||||
|
TenantUpdateStartAction |
|
||||||
|
TenantUpdateSuccessAction |
|
||||||
|
TenantUpdateFailureAction;
|
||||||
|
|
||||||
|
const tenantUpdateStart = (): TenantUpdateStartAction => ({
|
||||||
|
type: TENANT_UPDATE_START,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tenantUpdateSuccess = (
|
||||||
|
tenantJSON: ITenantJSON,
|
||||||
|
): TenantUpdateSuccessAction => ({
|
||||||
|
type: TENANT_UPDATE_SUCCESS,
|
||||||
|
tenant: tenantJSON,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tenantUpdateFailure = (error: string): TenantUpdateFailureAction => ({
|
||||||
|
type: TENANT_UPDATE_FAILURE,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UpdateTenantParams {
|
||||||
|
siteName?: string;
|
||||||
|
siteLogo?: string;
|
||||||
|
brandDisplaySetting?: string;
|
||||||
|
locale?: string;
|
||||||
|
authenticityToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateTenant = ({
|
||||||
|
siteName = null,
|
||||||
|
siteLogo = null,
|
||||||
|
brandDisplaySetting = null,
|
||||||
|
locale = null,
|
||||||
|
authenticityToken,
|
||||||
|
}: UpdateTenantParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
|
dispatch(tenantUpdateStart());
|
||||||
|
|
||||||
|
const tenant = Object.assign({},
|
||||||
|
siteName !== null ? { site_name: siteName } : null,
|
||||||
|
siteLogo !== null ? { site_logo: siteLogo } : null,
|
||||||
|
brandDisplaySetting !== null ? { brand_display_setting: brandDisplaySetting } : null,
|
||||||
|
locale !== null ? { locale } : null
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/tenants/0`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: buildRequestHeaders(authenticityToken),
|
||||||
|
body: JSON.stringify({ tenant }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (res.status === HttpStatus.OK) {
|
||||||
|
dispatch(tenantUpdateSuccess(json));
|
||||||
|
} else {
|
||||||
|
dispatch(tenantUpdateFailure(json.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(res);
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(tenantUpdateFailure(e));
|
||||||
|
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
65
app/javascript/actions/changeSiteSettingsGeneralForm.ts
Normal file
65
app/javascript/actions/changeSiteSettingsGeneralForm.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// siteName
|
||||||
|
export const SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_NAME = 'SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_NAME';
|
||||||
|
|
||||||
|
interface SiteSettingsChangeGeneralFormSiteName {
|
||||||
|
type: typeof SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_NAME,
|
||||||
|
siteName: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeSiteSettingsGeneralFormSiteName = (
|
||||||
|
siteName: string
|
||||||
|
): SiteSettingsChangeGeneralFormSiteName => ({
|
||||||
|
type: SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_NAME,
|
||||||
|
siteName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// siteLogo
|
||||||
|
export const SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_LOGO = 'SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_LOGO';
|
||||||
|
|
||||||
|
interface SiteSettingsChangeGeneralFormSiteLogo {
|
||||||
|
type: typeof SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_LOGO,
|
||||||
|
siteLogo: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeSiteSettingsGeneralFormSiteLogo = (
|
||||||
|
siteLogo: string
|
||||||
|
): SiteSettingsChangeGeneralFormSiteLogo => ({
|
||||||
|
type: SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_LOGO,
|
||||||
|
siteLogo,
|
||||||
|
});
|
||||||
|
|
||||||
|
// brandDisplaySetting
|
||||||
|
export const SITE_SETTINGS_CHANGE_GENERAL_FORM_BRAND_SETTING = 'SITE_SETTINGS_CHANGE_GENERAL_FORM_BRAND_SETTING';
|
||||||
|
|
||||||
|
interface SiteSettingsChangeGeneralFormBrandSetting {
|
||||||
|
type: typeof SITE_SETTINGS_CHANGE_GENERAL_FORM_BRAND_SETTING,
|
||||||
|
brandDisplaySetting: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeSiteSettingsGeneralFormBrandSetting = (
|
||||||
|
brandDisplaySetting: string
|
||||||
|
): SiteSettingsChangeGeneralFormBrandSetting => ({
|
||||||
|
type: SITE_SETTINGS_CHANGE_GENERAL_FORM_BRAND_SETTING,
|
||||||
|
brandDisplaySetting,
|
||||||
|
});
|
||||||
|
|
||||||
|
// locale
|
||||||
|
export const SITE_SETTINGS_CHANGE_GENERAL_FORM_LOCALE = 'SITE_SETTINGS_CHANGE_GENERAL_FORM_LOCALE';
|
||||||
|
|
||||||
|
interface SiteSettingsChangeGeneralFormLocale {
|
||||||
|
type: typeof SITE_SETTINGS_CHANGE_GENERAL_FORM_LOCALE,
|
||||||
|
locale: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeSiteSettingsGeneralFormLocale = (
|
||||||
|
locale: string
|
||||||
|
): SiteSettingsChangeGeneralFormLocale => ({
|
||||||
|
type: SITE_SETTINGS_CHANGE_GENERAL_FORM_LOCALE,
|
||||||
|
locale,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ChangeSiteSettingsGeneralFormActionTypes =
|
||||||
|
SiteSettingsChangeGeneralFormSiteName |
|
||||||
|
SiteSettingsChangeGeneralFormSiteLogo |
|
||||||
|
SiteSettingsChangeGeneralFormBrandSetting |
|
||||||
|
SiteSettingsChangeGeneralFormLocale;
|
||||||
@@ -80,18 +80,22 @@ class BoardForm extends React.Component<Props, State> {
|
|||||||
const {name, description} = this.state;
|
const {name, description} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="boardForm">
|
<form className="boardForm">
|
||||||
<div className="boardMandatoryForm">
|
<div className="boardMandatoryForm">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={I18n.t('site_settings.boards.form.name')}
|
placeholder={I18n.t('site_settings.boards.form.name')}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => this.onNameChange(e.target.value)}
|
onChange={e => this.onNameChange(e.target.value)}
|
||||||
|
autoFocus
|
||||||
className="form-control"
|
className="form-control"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={this.onSubmit}
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onSubmit();
|
||||||
|
}}
|
||||||
className="newBoardButton"
|
className="newBoardButton"
|
||||||
disabled={!this.isFormValid()}
|
disabled={!this.isFormValid()}
|
||||||
>
|
>
|
||||||
@@ -110,7 +114,7 @@ class BoardForm extends React.Component<Props, State> {
|
|||||||
onChange={e => this.onDescriptionChange(e.target.value)}
|
onChange={e => this.onDescriptionChange(e.target.value)}
|
||||||
className="form-control"
|
className="form-control"
|
||||||
/>
|
/>
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import I18n from 'i18n-js';
|
||||||
|
|
||||||
|
import Box from '../../common/Box';
|
||||||
|
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
|
||||||
|
import { ISiteSettingsGeneralForm } from '../../../reducers/SiteSettings/generalReducer';
|
||||||
|
import Button from '../../common/Button';
|
||||||
|
import HttpStatus from '../../../constants/http_status';
|
||||||
|
import {
|
||||||
|
TENANT_BRAND_NAME_AND_LOGO,
|
||||||
|
TENANT_BRAND_NAME_ONLY,
|
||||||
|
TENANT_BRAND_LOGO_ONLY,
|
||||||
|
TENANT_BRAND_NONE,
|
||||||
|
} from '../../../interfaces/ITenant';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
originForm: ISiteSettingsGeneralForm;
|
||||||
|
authenticityToken: string;
|
||||||
|
|
||||||
|
form: ISiteSettingsGeneralForm;
|
||||||
|
areDirty: boolean;
|
||||||
|
areLoading: boolean;
|
||||||
|
areUpdating: boolean;
|
||||||
|
error: string;
|
||||||
|
|
||||||
|
requestTenant(): void;
|
||||||
|
updateTenant(
|
||||||
|
siteName: string,
|
||||||
|
siteLogo: string,
|
||||||
|
brandDisplaySetting: string,
|
||||||
|
locale: string,
|
||||||
|
authenticityToken: string
|
||||||
|
): Promise<any>;
|
||||||
|
|
||||||
|
handleChangeSiteName(siteName: string): void;
|
||||||
|
handleChangeSiteLogo(siteLogo: string): void;
|
||||||
|
handleChangeBrandDisplaySetting(brandDisplaySetting: string)
|
||||||
|
handleChangeLocale(locale: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeneralSiteSettingsP extends React.Component<Props> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._handleUpdateTenant = this._handleUpdateTenant.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.requestTenant();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleUpdateTenant() {
|
||||||
|
const { siteName, siteLogo, brandDisplaySetting, locale } = this.props.form;
|
||||||
|
|
||||||
|
this.props.updateTenant(
|
||||||
|
siteName,
|
||||||
|
siteLogo,
|
||||||
|
brandDisplaySetting,
|
||||||
|
locale,
|
||||||
|
this.props.authenticityToken,
|
||||||
|
).then(res => {
|
||||||
|
if (res?.status !== HttpStatus.OK) return;
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
originForm,
|
||||||
|
form,
|
||||||
|
areDirty,
|
||||||
|
areLoading,
|
||||||
|
areUpdating,
|
||||||
|
error,
|
||||||
|
|
||||||
|
handleChangeSiteName,
|
||||||
|
handleChangeSiteLogo,
|
||||||
|
handleChangeBrandDisplaySetting,
|
||||||
|
handleChangeLocale,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box>
|
||||||
|
<h2>{ I18n.t('site_settings.general.title') }</h2>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div className="formRow">
|
||||||
|
<div className="formGroup col-4">
|
||||||
|
<label htmlFor="siteName">{ I18n.t('site_settings.general.site_name') }</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={areLoading ? originForm.siteName : form.siteName}
|
||||||
|
onChange={e => handleChangeSiteName(e.target.value)}
|
||||||
|
id="siteName"
|
||||||
|
className="formControl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formGroup col-4">
|
||||||
|
<label htmlFor="siteLogo">{ I18n.t('site_settings.general.site_logo') }</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={areLoading ? originForm.siteLogo : form.siteLogo}
|
||||||
|
onChange={e => handleChangeSiteLogo(e.target.value)}
|
||||||
|
id="siteLogo"
|
||||||
|
className="formControl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formGroup col-4">
|
||||||
|
<label htmlFor="brandSetting">{ I18n.t('site_settings.general.brand_setting') }</label>
|
||||||
|
<select
|
||||||
|
value={form.brandDisplaySetting || originForm.brandDisplaySetting}
|
||||||
|
onChange={e => handleChangeBrandDisplaySetting(e.target.value)}
|
||||||
|
id="brandSetting"
|
||||||
|
className="selectPicker"
|
||||||
|
>
|
||||||
|
<option value={TENANT_BRAND_NAME_AND_LOGO}>
|
||||||
|
{ I18n.t('site_settings.general.brand_setting_both') }
|
||||||
|
</option>
|
||||||
|
<option value={TENANT_BRAND_NAME_ONLY}>
|
||||||
|
{ I18n.t('site_settings.general.brand_setting_name') }
|
||||||
|
</option>
|
||||||
|
<option value={TENANT_BRAND_LOGO_ONLY}>
|
||||||
|
{ I18n.t('site_settings.general.brand_setting_logo') }
|
||||||
|
</option>
|
||||||
|
<option value={TENANT_BRAND_NONE}>
|
||||||
|
{ I18n.t('site_settings.general.brand_setting_none') }
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formGroup">
|
||||||
|
<label htmlFor="locale">{ I18n.t('site_settings.general.locale') }</label>
|
||||||
|
<select
|
||||||
|
value={form.locale || originForm.locale}
|
||||||
|
onChange={e => handleChangeLocale(e.target.value)}
|
||||||
|
id="locale"
|
||||||
|
className="selectPicker"
|
||||||
|
>
|
||||||
|
<option value="en">🇬🇧 English</option>
|
||||||
|
<option value="it">🇮🇹 Italiano</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={this._handleUpdateTenant}
|
||||||
|
disabled={!areDirty}
|
||||||
|
>
|
||||||
|
{ I18n.t('common.buttons.update') }
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<SiteSettingsInfoBox areUpdating={areLoading || areUpdating} error={error} areDirty={areDirty} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeneralSiteSettingsP;
|
||||||
36
app/javascript/components/SiteSettings/General/index.tsx
Normal file
36
app/javascript/components/SiteSettings/General/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { Store } from 'redux';
|
||||||
|
|
||||||
|
import GeneralSiteSettings from '../../../containers/GeneralSiteSettings';
|
||||||
|
import createStoreHelper from '../../../helpers/createStore';
|
||||||
|
import { State } from '../../../reducers/rootReducer';
|
||||||
|
import { ISiteSettingsGeneralForm } from '../../../reducers/SiteSettings/generalReducer';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
originForm: ISiteSettingsGeneralForm;
|
||||||
|
authenticityToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeneralSiteSettingsRoot extends React.Component<Props> {
|
||||||
|
store: Store<State, any>;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.store = createStoreHelper();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Provider store={this.store}>
|
||||||
|
<GeneralSiteSettings
|
||||||
|
originForm={this.props.originForm}
|
||||||
|
authenticityToken={this.props.authenticityToken}
|
||||||
|
/>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeneralSiteSettingsRoot;
|
||||||
@@ -87,12 +87,13 @@ class PostStatusForm extends React.Component<Props, State> {
|
|||||||
const {name, color} = this.state;
|
const {name, color} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="postStatusForm">
|
<form className="postStatusForm">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={I18n.t('site_settings.post_statuses.form.name')}
|
placeholder={I18n.t('site_settings.post_statuses.form.name')}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => this.onNameChange(e.target.value)}
|
onChange={e => this.onNameChange(e.target.value)}
|
||||||
|
autoFocus
|
||||||
className="form-control"
|
className="form-control"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -104,7 +105,10 @@ class PostStatusForm extends React.Component<Props, State> {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={this.onSubmit}
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onSubmit();
|
||||||
|
}}
|
||||||
className="newPostStatusButton"
|
className="newPostStatusButton"
|
||||||
disabled={!this.isFormValid()}
|
disabled={!this.isFormValid()}
|
||||||
>
|
>
|
||||||
@@ -115,7 +119,7 @@ class PostStatusForm extends React.Component<Props, State> {
|
|||||||
I18n.t('common.buttons.update')
|
I18n.t('common.buttons.update')
|
||||||
}
|
}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
app/javascript/components/TenantSignUp/ConfirmSignUpPage.tsx
Normal file
21
app/javascript/components/TenantSignUp/ConfirmSignUpPage.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import I18n from 'i18n-js';
|
||||||
|
import Box from '../common/Box';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
subdomain: string;
|
||||||
|
userEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfirmSignUpPage = ({
|
||||||
|
subdomain,
|
||||||
|
userEmail,
|
||||||
|
}: Props) => (
|
||||||
|
<Box>
|
||||||
|
<h3>{ I18n.t('signup.step3.title') }</h3>
|
||||||
|
|
||||||
|
<p>{ I18n.t('signup.step3.message', { email: userEmail, subdomain: `${subdomain}.astuto.io` }) }</p>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ConfirmSignUpPage;
|
||||||
92
app/javascript/components/TenantSignUp/TenantSignUpForm.tsx
Normal file
92
app/javascript/components/TenantSignUp/TenantSignUpForm.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import I18n from 'i18n-js';
|
||||||
|
|
||||||
|
import Box from '../common/Box';
|
||||||
|
import { TenantSignUpTenantFormState } from '../../reducers/tenantSignUpReducer';
|
||||||
|
import Button from '../common/Button';
|
||||||
|
import Spinner from '../common/Spinner';
|
||||||
|
import { DangerText } from '../common/CustomTexts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tenantForm: TenantSignUpTenantFormState;
|
||||||
|
handleChangeTenantSiteName(siteName: string): void;
|
||||||
|
handleChangeTenantSubdomain(subdomain: string): void;
|
||||||
|
|
||||||
|
isSubmitting: boolean;
|
||||||
|
error: string;
|
||||||
|
handleSubmit(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TenantSignUpForm extends React.Component<Props> {
|
||||||
|
form: any;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.form = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
tenantForm,
|
||||||
|
handleChangeTenantSiteName,
|
||||||
|
handleChangeTenantSubdomain,
|
||||||
|
|
||||||
|
isSubmitting,
|
||||||
|
error,
|
||||||
|
handleSubmit,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box customClass="tenantSignUpStep2">
|
||||||
|
<h3>{ I18n.t('signup.step2.title') }</h3>
|
||||||
|
|
||||||
|
<form ref={this.form}>
|
||||||
|
<div className="formRow">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
value={tenantForm.siteName}
|
||||||
|
onChange={e => handleChangeTenantSiteName(e.target.value)}
|
||||||
|
placeholder={I18n.t('signup.step2.site_name')}
|
||||||
|
required
|
||||||
|
id="tenantSiteName"
|
||||||
|
className="formControl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formRow">
|
||||||
|
<div className="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={tenantForm.subdomain}
|
||||||
|
onChange={e => handleChangeTenantSubdomain(e.target.value)}
|
||||||
|
placeholder={I18n.t('signup.step2.subdomain')}
|
||||||
|
required
|
||||||
|
id="tenantSubdomain"
|
||||||
|
className="formControl"
|
||||||
|
/>
|
||||||
|
<div className="input-group-append">
|
||||||
|
<div className="input-group-text">.astuto.io</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
}}
|
||||||
|
className="tenantConfirm"
|
||||||
|
>
|
||||||
|
{ isSubmitting ? <Spinner /> : I18n.t('signup.step2.create_button') }
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{ error !== '' && <DangerText>{ error }</DangerText> }
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TenantSignUpForm;
|
||||||
124
app/javascript/components/TenantSignUp/TenantSignUpP.tsx
Normal file
124
app/javascript/components/TenantSignUp/TenantSignUpP.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { TenantSignUpTenantFormState, TenantSignUpUserFormState } from '../../reducers/tenantSignUpReducer';
|
||||||
|
import ConfirmSignUpPage from './ConfirmSignUpPage';
|
||||||
|
|
||||||
|
import TenantSignUpForm from './TenantSignUpForm';
|
||||||
|
import UserSignUpForm from './UserSignUpForm';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
authenticityToken: string;
|
||||||
|
|
||||||
|
currentStep: number;
|
||||||
|
emailAuth: boolean;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
error: string;
|
||||||
|
|
||||||
|
toggleEmailAuth(): void;
|
||||||
|
|
||||||
|
userForm: TenantSignUpUserFormState;
|
||||||
|
handleChangeUserFullName(fullName: string): void;
|
||||||
|
handleChangeUserEmail(email: string): void;
|
||||||
|
handleChangeUserPassword(password: string): void;
|
||||||
|
handleChangeUserPasswordConfirmation(passwordConfirmation: string): void;
|
||||||
|
handleUserFormConfirm(): void;
|
||||||
|
|
||||||
|
tenantForm: TenantSignUpTenantFormState;
|
||||||
|
handleChangeTenantSiteName(siteName: string): void;
|
||||||
|
handleChangeTenantSubdomain(subdomain: string): void;
|
||||||
|
|
||||||
|
handleSubmit(
|
||||||
|
userFullName: string,
|
||||||
|
userEmail: string,
|
||||||
|
userPassword: string,
|
||||||
|
siteName: string,
|
||||||
|
subdomain: string,
|
||||||
|
authenticityToken: string,
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TenantSignUpP extends React.Component<Props> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._handleSubmit = this._handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleSubmit() {
|
||||||
|
const { userForm, tenantForm, handleSubmit } = this.props;
|
||||||
|
|
||||||
|
handleSubmit(
|
||||||
|
userForm.fullName,
|
||||||
|
userForm.email,
|
||||||
|
userForm.password,
|
||||||
|
tenantForm.siteName,
|
||||||
|
tenantForm.subdomain,
|
||||||
|
this.props.authenticityToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
currentStep,
|
||||||
|
emailAuth,
|
||||||
|
toggleEmailAuth,
|
||||||
|
|
||||||
|
userForm,
|
||||||
|
handleChangeUserFullName,
|
||||||
|
handleChangeUserEmail,
|
||||||
|
handleChangeUserPassword,
|
||||||
|
handleChangeUserPasswordConfirmation,
|
||||||
|
handleUserFormConfirm,
|
||||||
|
|
||||||
|
tenantForm,
|
||||||
|
handleChangeTenantSiteName,
|
||||||
|
handleChangeTenantSubdomain,
|
||||||
|
|
||||||
|
isSubmitting,
|
||||||
|
error,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tenantSignUpContainer">
|
||||||
|
{
|
||||||
|
(currentStep === 1 || currentStep === 2) &&
|
||||||
|
<UserSignUpForm
|
||||||
|
currentStep={currentStep}
|
||||||
|
|
||||||
|
emailAuth={emailAuth}
|
||||||
|
toggleEmailAuth={toggleEmailAuth}
|
||||||
|
userForm={userForm}
|
||||||
|
|
||||||
|
handleChangeUserFullName={handleChangeUserFullName}
|
||||||
|
handleChangeUserEmail={handleChangeUserEmail}
|
||||||
|
handleChangeUserPassword={handleChangeUserPassword}
|
||||||
|
handleChangeUserPasswordConfirmation={handleChangeUserPasswordConfirmation}
|
||||||
|
handleUserFormConfirm={handleUserFormConfirm}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
currentStep === 2 &&
|
||||||
|
<TenantSignUpForm
|
||||||
|
tenantForm={tenantForm}
|
||||||
|
handleChangeTenantSiteName={handleChangeTenantSiteName}
|
||||||
|
handleChangeTenantSubdomain={handleChangeTenantSubdomain}
|
||||||
|
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
error={error}
|
||||||
|
handleSubmit={this._handleSubmit}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
currentStep === 3 &&
|
||||||
|
<ConfirmSignUpPage
|
||||||
|
subdomain={tenantForm.subdomain}
|
||||||
|
userEmail={userForm.email}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TenantSignUpP;
|
||||||
147
app/javascript/components/TenantSignUp/UserSignUpForm.tsx
Normal file
147
app/javascript/components/TenantSignUp/UserSignUpForm.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import I18n from 'i18n-js';
|
||||||
|
|
||||||
|
import Box from '../common/Box';
|
||||||
|
import Button from '../common/Button';
|
||||||
|
import { TenantSignUpUserFormState } from '../../reducers/tenantSignUpReducer';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentStep: number;
|
||||||
|
emailAuth: boolean;
|
||||||
|
toggleEmailAuth(): void;
|
||||||
|
userForm: TenantSignUpUserFormState;
|
||||||
|
|
||||||
|
handleChangeUserFullName(fullName: string): void;
|
||||||
|
handleChangeUserEmail(email: string): void;
|
||||||
|
handleChangeUserPassword(password: string): void;
|
||||||
|
handleChangeUserPasswordConfirmation(passwordConfirmation: string): void;
|
||||||
|
handleUserFormConfirm(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserSignUpForm extends React.Component<Props> {
|
||||||
|
form: any;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.form = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
validateUserForm(): boolean {
|
||||||
|
let isValid: boolean = this.form.current.reportValidity();
|
||||||
|
if (this.validateUserPasswordConfirmation() === false)
|
||||||
|
isValid = false;
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateUserPasswordConfirmation(): boolean {
|
||||||
|
const isValid = this.props.userForm.password === this.props.userForm.passwordConfirmation;
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
currentStep,
|
||||||
|
emailAuth,
|
||||||
|
toggleEmailAuth,
|
||||||
|
userForm,
|
||||||
|
|
||||||
|
handleChangeUserFullName,
|
||||||
|
handleChangeUserEmail,
|
||||||
|
handleChangeUserPassword,
|
||||||
|
handleChangeUserPasswordConfirmation,
|
||||||
|
handleUserFormConfirm,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box customClass="tenantSignUpStep1">
|
||||||
|
<h3>{ I18n.t('signup.step1.title') }</h3>
|
||||||
|
|
||||||
|
{
|
||||||
|
currentStep === 1 && !emailAuth &&
|
||||||
|
<Button className="emailAuth" onClick={toggleEmailAuth}>
|
||||||
|
{ I18n.t('signup.step1.email_auth') }
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
currentStep === 1 && emailAuth &&
|
||||||
|
<form ref={this.form}>
|
||||||
|
<div className="formRow">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
value={userForm.fullName}
|
||||||
|
onChange={e => handleChangeUserFullName(e.target.value)}
|
||||||
|
placeholder={I18n.t('common.forms.auth.full_name')}
|
||||||
|
required
|
||||||
|
id="userFullName"
|
||||||
|
className="formControl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formRow">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={userForm.email}
|
||||||
|
onChange={e => handleChangeUserEmail(e.target.value)}
|
||||||
|
placeholder={I18n.t('common.forms.auth.email')}
|
||||||
|
required
|
||||||
|
id="userEmail"
|
||||||
|
className="formControl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formRow">
|
||||||
|
<div className="formGroup col-6">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={userForm.password}
|
||||||
|
onChange={e => handleChangeUserPassword(e.target.value)}
|
||||||
|
placeholder={I18n.t('common.forms.auth.password')}
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
maxLength={128}
|
||||||
|
id="userPassword"
|
||||||
|
className="formControl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="formGroup col-6">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={userForm.passwordConfirmation}
|
||||||
|
onChange={e => handleChangeUserPasswordConfirmation(e.target.value)}
|
||||||
|
placeholder={I18n.t('common.forms.auth.password_confirmation')}
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
maxLength={128}
|
||||||
|
id="userPasswordConfirmation"
|
||||||
|
className={`formControl${userForm.passwordConfirmationError ? ' invalid' : ''}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.validateUserForm() && handleUserFormConfirm();
|
||||||
|
}}
|
||||||
|
className="userConfirm"
|
||||||
|
>
|
||||||
|
{ I18n.t('common.buttons.confirm') }
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
currentStep === 2 &&
|
||||||
|
<p><b>{userForm.fullName}</b> ({userForm.email})</p>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserSignUpForm;
|
||||||
37
app/javascript/components/TenantSignUp/index.tsx
Normal file
37
app/javascript/components/TenantSignUp/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import createStoreHelper from '../../helpers/createStore';
|
||||||
|
|
||||||
|
import TenantSignUp from '../../containers/TenantSignUp';
|
||||||
|
|
||||||
|
import { Store } from 'redux';
|
||||||
|
import { State } from '../../reducers/rootReducer';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
authenticityToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TenantSignUpRoot extends React.Component<Props> {
|
||||||
|
store: Store<State, any>;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.store = createStoreHelper();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { authenticityToken } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={this.store}>
|
||||||
|
<TenantSignUp
|
||||||
|
authenticityToken={authenticityToken}
|
||||||
|
/>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TenantSignUpRoot;
|
||||||
@@ -7,9 +7,10 @@ import Box from './Box';
|
|||||||
interface Props {
|
interface Props {
|
||||||
areUpdating: boolean;
|
areUpdating: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
|
areDirty?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SiteSettingsInfoBox = ({ areUpdating, error }: Props) => (
|
const SiteSettingsInfoBox = ({ areUpdating, error, areDirty = false }: Props) => (
|
||||||
<Box customClass="siteSettingsInfo">
|
<Box customClass="siteSettingsInfo">
|
||||||
{
|
{
|
||||||
areUpdating ?
|
areUpdating ?
|
||||||
@@ -17,10 +18,13 @@ const SiteSettingsInfoBox = ({ areUpdating, error }: Props) => (
|
|||||||
:
|
:
|
||||||
error ?
|
error ?
|
||||||
<span className="error">
|
<span className="error">
|
||||||
{I18n.t('site_settings.info_box.error', { message: JSON.stringify(error) })}
|
{ I18n.t('site_settings.info_box.error', { message: JSON.stringify(error) }) }
|
||||||
</span>
|
</span>
|
||||||
:
|
:
|
||||||
<span>{I18n.t('site_settings.info_box.up_to_date')}</span>
|
areDirty ?
|
||||||
|
<span className="warning">{ I18n.t('site_settings.info_box.dirty') }</span>
|
||||||
|
:
|
||||||
|
<span>{ I18n.t('site_settings.info_box.up_to_date') }</span>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const POSTS_PER_PAGE = parseInt(process.env.POSTS_PER_PAGE);
|
export const POSTS_PER_PAGE = 15;
|
||||||
63
app/javascript/containers/GeneralSiteSettings.tsx
Normal file
63
app/javascript/containers/GeneralSiteSettings.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
import { requestTenant } from "../actions/Tenant/requestTenant";
|
||||||
|
import {
|
||||||
|
changeSiteSettingsGeneralFormBrandSetting,
|
||||||
|
changeSiteSettingsGeneralFormLocale,
|
||||||
|
changeSiteSettingsGeneralFormSiteLogo,
|
||||||
|
changeSiteSettingsGeneralFormSiteName
|
||||||
|
} from "../actions/changeSiteSettingsGeneralForm";
|
||||||
|
import GeneralSiteSettingsP from "../components/SiteSettings/General/GeneralSiteSettingsP";
|
||||||
|
import { State } from "../reducers/rootReducer";
|
||||||
|
import { updateTenant } from "../actions/Tenant/updateTenant";
|
||||||
|
|
||||||
|
const mapStateToProps = (state: State) => ({
|
||||||
|
form: state.siteSettings.general.form,
|
||||||
|
areDirty: state.siteSettings.general.areDirty,
|
||||||
|
areLoading: state.siteSettings.general.areLoading,
|
||||||
|
areUpdating: state.siteSettings.general.areUpdating,
|
||||||
|
error: state.siteSettings.general.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => ({
|
||||||
|
requestTenant() {
|
||||||
|
dispatch(requestTenant());
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTenant(
|
||||||
|
siteName: string,
|
||||||
|
siteLogo: string,
|
||||||
|
brandDisplaySetting: string,
|
||||||
|
locale: string,
|
||||||
|
authenticityToken: string
|
||||||
|
): Promise<any> {
|
||||||
|
return dispatch(updateTenant({
|
||||||
|
siteName,
|
||||||
|
siteLogo,
|
||||||
|
brandDisplaySetting,
|
||||||
|
locale,
|
||||||
|
authenticityToken,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeSiteName(siteName: string) {
|
||||||
|
dispatch(changeSiteSettingsGeneralFormSiteName(siteName));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeSiteLogo(siteLogo: string) {
|
||||||
|
dispatch(changeSiteSettingsGeneralFormSiteLogo(siteLogo));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeBrandDisplaySetting(brandDisplaySetting: string) {
|
||||||
|
dispatch(changeSiteSettingsGeneralFormBrandSetting(brandDisplaySetting));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeLocale(locale: string) {
|
||||||
|
dispatch(changeSiteSettingsGeneralFormLocale(locale));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(GeneralSiteSettingsP);
|
||||||
82
app/javascript/containers/TenantSignUp.tsx
Normal file
82
app/javascript/containers/TenantSignUp.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
import TenantSignUpP from "../components/TenantSignUp/TenantSignUpP";
|
||||||
|
|
||||||
|
import { State } from "../reducers/rootReducer";
|
||||||
|
import {
|
||||||
|
changeTenantSiteNameTenantSignUp,
|
||||||
|
changeTenantSubdomainTenantSignUp,
|
||||||
|
changeUserEmailTenantSignUp,
|
||||||
|
changeUserFullNameTenantSignUp,
|
||||||
|
changeUserPasswordConfirmationTenantSignUp,
|
||||||
|
changeUserPasswordTenantSignUp,
|
||||||
|
confirmUserFormTenantSignUp,
|
||||||
|
toggleEmailAuthTenantSignUp
|
||||||
|
} from "../actions/Tenant/tenantSignUpFormActions";
|
||||||
|
import { submitTenant } from "../actions/Tenant/submitTenant";
|
||||||
|
|
||||||
|
const mapStateToProps = (state: State) => ({
|
||||||
|
currentStep: state.tenantSignUp.currentStep,
|
||||||
|
emailAuth: state.tenantSignUp.emailAuth,
|
||||||
|
isSubmitting: state.tenantSignUp.isSubmitting,
|
||||||
|
error: state.tenantSignUp.error,
|
||||||
|
userForm: state.tenantSignUp.userForm,
|
||||||
|
tenantForm: state.tenantSignUp.tenantForm,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: any) => ({
|
||||||
|
toggleEmailAuth() {
|
||||||
|
dispatch(toggleEmailAuthTenantSignUp());
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeUserFullName(fullName: string) {
|
||||||
|
dispatch(changeUserFullNameTenantSignUp(fullName));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeUserEmail(email: string) {
|
||||||
|
dispatch(changeUserEmailTenantSignUp(email));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeUserPassword(password: string) {
|
||||||
|
dispatch(changeUserPasswordTenantSignUp(password));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeUserPasswordConfirmation(passwordConfirmation: string) {
|
||||||
|
dispatch(changeUserPasswordConfirmationTenantSignUp(passwordConfirmation));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUserFormConfirm() {
|
||||||
|
dispatch(confirmUserFormTenantSignUp());
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeTenantSiteName(siteName: string) {
|
||||||
|
dispatch(changeTenantSiteNameTenantSignUp(siteName));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChangeTenantSubdomain(subdomain: string) {
|
||||||
|
dispatch(changeTenantSubdomainTenantSignUp(subdomain));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSubmit(
|
||||||
|
userFullName: string,
|
||||||
|
userEmail: string,
|
||||||
|
userPassword: string,
|
||||||
|
siteName: string,
|
||||||
|
subdomain: string,
|
||||||
|
authenticityToken: string,
|
||||||
|
) {
|
||||||
|
dispatch(submitTenant(
|
||||||
|
userFullName,
|
||||||
|
userEmail,
|
||||||
|
userPassword,
|
||||||
|
siteName,
|
||||||
|
subdomain,
|
||||||
|
authenticityToken,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(TenantSignUpP);
|
||||||
21
app/javascript/interfaces/ITenant.ts
Normal file
21
app/javascript/interfaces/ITenant.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Brand display setting
|
||||||
|
export const TENANT_BRAND_NAME_AND_LOGO = 'name_and_logo';
|
||||||
|
export const TENANT_BRAND_NAME_ONLY = 'name_only';
|
||||||
|
export const TENANT_BRAND_LOGO_ONLY = 'logo_only';
|
||||||
|
export const TENANT_BRAND_NONE = 'no_name_no_logo';
|
||||||
|
|
||||||
|
export type TenantBrandDisplaySetting =
|
||||||
|
typeof TENANT_BRAND_NAME_AND_LOGO |
|
||||||
|
typeof TENANT_BRAND_NAME_ONLY |
|
||||||
|
typeof TENANT_BRAND_LOGO_ONLY |
|
||||||
|
typeof TENANT_BRAND_NONE;
|
||||||
|
|
||||||
|
interface ITenant {
|
||||||
|
id: number;
|
||||||
|
siteName: string;
|
||||||
|
siteLogo: string;
|
||||||
|
brandDisplaySetting: TenantBrandDisplaySetting;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ITenant;
|
||||||
9
app/javascript/interfaces/json/ITenant.ts
Normal file
9
app/javascript/interfaces/json/ITenant.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
interface ITenantJSON {
|
||||||
|
id: number;
|
||||||
|
site_name: string;
|
||||||
|
site_logo: string;
|
||||||
|
brand_display_setting: string;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ITenantJSON;
|
||||||
140
app/javascript/reducers/SiteSettings/generalReducer.ts
Normal file
140
app/javascript/reducers/SiteSettings/generalReducer.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
TenantRequestActionTypes,
|
||||||
|
TENANT_REQUEST_START,
|
||||||
|
TENANT_REQUEST_SUCCESS,
|
||||||
|
TENANT_REQUEST_FAILURE,
|
||||||
|
} from "../../actions/Tenant/requestTenant";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TenantUpdateActionTypes,
|
||||||
|
TENANT_UPDATE_START,
|
||||||
|
TENANT_UPDATE_SUCCESS,
|
||||||
|
TENANT_UPDATE_FAILURE,
|
||||||
|
} from '../../actions/Tenant/updateTenant';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChangeSiteSettingsGeneralFormActionTypes,
|
||||||
|
SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_NAME,
|
||||||
|
SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_LOGO,
|
||||||
|
SITE_SETTINGS_CHANGE_GENERAL_FORM_BRAND_SETTING,
|
||||||
|
SITE_SETTINGS_CHANGE_GENERAL_FORM_LOCALE,
|
||||||
|
} from '../../actions/changeSiteSettingsGeneralForm';
|
||||||
|
|
||||||
|
export interface ISiteSettingsGeneralForm {
|
||||||
|
siteName: string;
|
||||||
|
siteLogo: string;
|
||||||
|
brandDisplaySetting: string;
|
||||||
|
locale: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SiteSettingsGeneralState {
|
||||||
|
form: ISiteSettingsGeneralForm,
|
||||||
|
areDirty: boolean;
|
||||||
|
areLoading: boolean;
|
||||||
|
areUpdating: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SiteSettingsGeneralState = {
|
||||||
|
form: {
|
||||||
|
siteName: '',
|
||||||
|
siteLogo: '',
|
||||||
|
brandDisplaySetting: '',
|
||||||
|
locale: '',
|
||||||
|
},
|
||||||
|
areDirty: false,
|
||||||
|
areLoading: false,
|
||||||
|
areUpdating: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const siteSettingsGeneralReducer = (
|
||||||
|
state = initialState,
|
||||||
|
action:
|
||||||
|
TenantRequestActionTypes |
|
||||||
|
TenantUpdateActionTypes |
|
||||||
|
ChangeSiteSettingsGeneralFormActionTypes
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case TENANT_REQUEST_START:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areLoading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_UPDATE_START:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areUpdating: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_REQUEST_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
form: {
|
||||||
|
siteName: action.tenant.site_name,
|
||||||
|
siteLogo: action.tenant.site_logo,
|
||||||
|
brandDisplaySetting: action.tenant.brand_display_setting,
|
||||||
|
locale: action.tenant.locale,
|
||||||
|
},
|
||||||
|
areDirty: false,
|
||||||
|
areLoading: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_UPDATE_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areDirty: false,
|
||||||
|
areUpdating: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_REQUEST_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areLoading: false,
|
||||||
|
error: action.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_UPDATE_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areUpdating: false,
|
||||||
|
error: action.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
case SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_NAME:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
form: { ...state.form, siteName: action.siteName },
|
||||||
|
areDirty: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_LOGO:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
form: { ...state.form, siteLogo: action.siteLogo },
|
||||||
|
areDirty: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case SITE_SETTINGS_CHANGE_GENERAL_FORM_BRAND_SETTING:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
form: { ...state.form, brandDisplaySetting: action.brandDisplaySetting },
|
||||||
|
areDirty: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case SITE_SETTINGS_CHANGE_GENERAL_FORM_LOCALE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
form: { ...state.form, locale: action.locale },
|
||||||
|
areDirty: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default siteSettingsGeneralReducer;
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import {
|
||||||
|
PostStatusesRequestActionTypes,
|
||||||
|
POST_STATUSES_REQUEST_START,
|
||||||
|
POST_STATUSES_REQUEST_SUCCESS,
|
||||||
|
POST_STATUSES_REQUEST_FAILURE,
|
||||||
|
} from '../../actions/PostStatus/requestPostStatuses';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PostStatusOrderUpdateActionTypes,
|
PostStatusOrderUpdateActionTypes,
|
||||||
POSTSTATUS_ORDER_UPDATE_START,
|
POSTSTATUS_ORDER_UPDATE_START,
|
||||||
@@ -38,12 +45,15 @@ const initialState: SiteSettingsPostStatusesState = {
|
|||||||
|
|
||||||
const siteSettingsPostStatusesReducer = (
|
const siteSettingsPostStatusesReducer = (
|
||||||
state = initialState,
|
state = initialState,
|
||||||
action: PostStatusOrderUpdateActionTypes |
|
action:
|
||||||
|
PostStatusesRequestActionTypes |
|
||||||
|
PostStatusOrderUpdateActionTypes |
|
||||||
PostStatusDeleteActionTypes |
|
PostStatusDeleteActionTypes |
|
||||||
PostStatusSubmitActionTypes |
|
PostStatusSubmitActionTypes |
|
||||||
PostStatusUpdateActionTypes
|
PostStatusUpdateActionTypes
|
||||||
): SiteSettingsPostStatusesState => {
|
): SiteSettingsPostStatusesState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case POST_STATUSES_REQUEST_START:
|
||||||
case POSTSTATUS_SUBMIT_START:
|
case POSTSTATUS_SUBMIT_START:
|
||||||
case POSTSTATUS_UPDATE_START:
|
case POSTSTATUS_UPDATE_START:
|
||||||
case POSTSTATUS_ORDER_UPDATE_START:
|
case POSTSTATUS_ORDER_UPDATE_START:
|
||||||
@@ -53,6 +63,7 @@ const siteSettingsPostStatusesReducer = (
|
|||||||
areUpdating: true,
|
areUpdating: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case POST_STATUSES_REQUEST_SUCCESS:
|
||||||
case POSTSTATUS_SUBMIT_SUCCESS:
|
case POSTSTATUS_SUBMIT_SUCCESS:
|
||||||
case POSTSTATUS_UPDATE_SUCCESS:
|
case POSTSTATUS_UPDATE_SUCCESS:
|
||||||
case POSTSTATUS_ORDER_UPDATE_SUCCESS:
|
case POSTSTATUS_ORDER_UPDATE_SUCCESS:
|
||||||
@@ -63,6 +74,7 @@ const siteSettingsPostStatusesReducer = (
|
|||||||
error: '',
|
error: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case POST_STATUSES_REQUEST_FAILURE:
|
||||||
case POSTSTATUS_SUBMIT_FAILURE:
|
case POSTSTATUS_SUBMIT_FAILURE:
|
||||||
case POSTSTATUS_UPDATE_FAILURE:
|
case POSTSTATUS_UPDATE_FAILURE:
|
||||||
case POSTSTATUS_ORDER_UPDATE_FAILURE:
|
case POSTSTATUS_ORDER_UPDATE_FAILURE:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
|
import tenantSignUpReducer from './tenantSignUpReducer';
|
||||||
|
|
||||||
import postsReducer from './postsReducer';
|
import postsReducer from './postsReducer';
|
||||||
import boardsReducer from './boardsReducer';
|
import boardsReducer from './boardsReducer';
|
||||||
import postStatusesReducer from './postStatusesReducer';
|
import postStatusesReducer from './postStatusesReducer';
|
||||||
@@ -8,6 +10,8 @@ import currentPostReducer from './currentPostReducer';
|
|||||||
import siteSettingsReducer from './siteSettingsReducer';
|
import siteSettingsReducer from './siteSettingsReducer';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
|
tenantSignUp: tenantSignUpReducer,
|
||||||
|
|
||||||
posts: postsReducer,
|
posts: postsReducer,
|
||||||
boards: boardsReducer,
|
boards: boardsReducer,
|
||||||
postStatuses: postStatusesReducer,
|
postStatuses: postStatusesReducer,
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
import {
|
||||||
|
TenantRequestActionTypes,
|
||||||
|
TENANT_REQUEST_START,
|
||||||
|
TENANT_REQUEST_SUCCESS,
|
||||||
|
TENANT_REQUEST_FAILURE,
|
||||||
|
} from '../actions/Tenant/requestTenant';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TenantUpdateActionTypes,
|
||||||
|
TENANT_UPDATE_START,
|
||||||
|
TENANT_UPDATE_SUCCESS,
|
||||||
|
TENANT_UPDATE_FAILURE,
|
||||||
|
} from '../actions/Tenant/updateTenant';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChangeSiteSettingsGeneralFormActionTypes,
|
||||||
|
SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_NAME,
|
||||||
|
SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_LOGO,
|
||||||
|
SITE_SETTINGS_CHANGE_GENERAL_FORM_BRAND_SETTING,
|
||||||
|
SITE_SETTINGS_CHANGE_GENERAL_FORM_LOCALE,
|
||||||
|
} from '../actions/changeSiteSettingsGeneralForm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BoardsRequestActionTypes,
|
BoardsRequestActionTypes,
|
||||||
BOARDS_REQUEST_START,
|
BOARDS_REQUEST_START,
|
||||||
@@ -33,6 +55,13 @@ import {
|
|||||||
BOARD_DELETE_FAILURE,
|
BOARD_DELETE_FAILURE,
|
||||||
} from '../actions/Board/deleteBoard';
|
} from '../actions/Board/deleteBoard';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PostStatusesRequestActionTypes,
|
||||||
|
POST_STATUSES_REQUEST_START,
|
||||||
|
POST_STATUSES_REQUEST_SUCCESS,
|
||||||
|
POST_STATUSES_REQUEST_FAILURE,
|
||||||
|
} from '../actions/PostStatus/requestPostStatuses';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PostStatusOrderUpdateActionTypes,
|
PostStatusOrderUpdateActionTypes,
|
||||||
POSTSTATUS_ORDER_UPDATE_START,
|
POSTSTATUS_ORDER_UPDATE_START,
|
||||||
@@ -75,12 +104,14 @@ import {
|
|||||||
USER_UPDATE_FAILURE,
|
USER_UPDATE_FAILURE,
|
||||||
} from '../actions/User/updateUser';
|
} from '../actions/User/updateUser';
|
||||||
|
|
||||||
|
import siteSettingsGeneralReducer, { SiteSettingsGeneralState } from './SiteSettings/generalReducer';
|
||||||
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
|
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
|
||||||
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
|
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
|
||||||
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
|
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
|
||||||
import siteSettingsUsersReducer, { SiteSettingsUsersState } from './SiteSettings/usersReducer';
|
import siteSettingsUsersReducer, { SiteSettingsUsersState } from './SiteSettings/usersReducer';
|
||||||
|
|
||||||
interface SiteSettingsState {
|
interface SiteSettingsState {
|
||||||
|
general: SiteSettingsGeneralState;
|
||||||
boards: SiteSettingsBoardsState;
|
boards: SiteSettingsBoardsState;
|
||||||
postStatuses: SiteSettingsPostStatusesState;
|
postStatuses: SiteSettingsPostStatusesState;
|
||||||
roadmap: SiteSettingsRoadmapState;
|
roadmap: SiteSettingsRoadmapState;
|
||||||
@@ -88,6 +119,7 @@ interface SiteSettingsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SiteSettingsState = {
|
const initialState: SiteSettingsState = {
|
||||||
|
general: siteSettingsGeneralReducer(undefined, {} as any),
|
||||||
boards: siteSettingsBoardsReducer(undefined, {} as any),
|
boards: siteSettingsBoardsReducer(undefined, {} as any),
|
||||||
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
|
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
|
||||||
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
|
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
|
||||||
@@ -97,11 +129,15 @@ const initialState: SiteSettingsState = {
|
|||||||
const siteSettingsReducer = (
|
const siteSettingsReducer = (
|
||||||
state = initialState,
|
state = initialState,
|
||||||
action:
|
action:
|
||||||
|
TenantRequestActionTypes |
|
||||||
|
TenantUpdateActionTypes |
|
||||||
|
ChangeSiteSettingsGeneralFormActionTypes |
|
||||||
BoardsRequestActionTypes |
|
BoardsRequestActionTypes |
|
||||||
BoardSubmitActionTypes |
|
BoardSubmitActionTypes |
|
||||||
BoardUpdateActionTypes |
|
BoardUpdateActionTypes |
|
||||||
BoardOrderUpdateActionTypes |
|
BoardOrderUpdateActionTypes |
|
||||||
BoardDeleteActionTypes |
|
BoardDeleteActionTypes |
|
||||||
|
PostStatusesRequestActionTypes |
|
||||||
PostStatusOrderUpdateActionTypes |
|
PostStatusOrderUpdateActionTypes |
|
||||||
PostStatusDeleteActionTypes |
|
PostStatusDeleteActionTypes |
|
||||||
PostStatusSubmitActionTypes |
|
PostStatusSubmitActionTypes |
|
||||||
@@ -110,6 +146,21 @@ const siteSettingsReducer = (
|
|||||||
UserUpdateActionTypes
|
UserUpdateActionTypes
|
||||||
): SiteSettingsState => {
|
): SiteSettingsState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
|
case TENANT_REQUEST_START:
|
||||||
|
case TENANT_REQUEST_SUCCESS:
|
||||||
|
case TENANT_REQUEST_FAILURE:
|
||||||
|
case TENANT_UPDATE_START:
|
||||||
|
case TENANT_UPDATE_SUCCESS:
|
||||||
|
case TENANT_UPDATE_FAILURE:
|
||||||
|
case SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_NAME:
|
||||||
|
case SITE_SETTINGS_CHANGE_GENERAL_FORM_SITE_LOGO:
|
||||||
|
case SITE_SETTINGS_CHANGE_GENERAL_FORM_BRAND_SETTING:
|
||||||
|
case SITE_SETTINGS_CHANGE_GENERAL_FORM_LOCALE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
general: siteSettingsGeneralReducer(state.general, action),
|
||||||
|
};
|
||||||
|
|
||||||
case BOARDS_REQUEST_START:
|
case BOARDS_REQUEST_START:
|
||||||
case BOARDS_REQUEST_SUCCESS:
|
case BOARDS_REQUEST_SUCCESS:
|
||||||
case BOARDS_REQUEST_FAILURE:
|
case BOARDS_REQUEST_FAILURE:
|
||||||
@@ -130,6 +181,9 @@ const siteSettingsReducer = (
|
|||||||
boards: siteSettingsBoardsReducer(state.boards, action),
|
boards: siteSettingsBoardsReducer(state.boards, action),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case POST_STATUSES_REQUEST_START:
|
||||||
|
case POST_STATUSES_REQUEST_SUCCESS:
|
||||||
|
case POST_STATUSES_REQUEST_FAILURE:
|
||||||
case POSTSTATUS_SUBMIT_START:
|
case POSTSTATUS_SUBMIT_START:
|
||||||
case POSTSTATUS_SUBMIT_SUCCESS:
|
case POSTSTATUS_SUBMIT_SUCCESS:
|
||||||
case POSTSTATUS_SUBMIT_FAILURE:
|
case POSTSTATUS_SUBMIT_FAILURE:
|
||||||
|
|||||||
145
app/javascript/reducers/tenantSignUpReducer.ts
Normal file
145
app/javascript/reducers/tenantSignUpReducer.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
TenantSignUpFormActions,
|
||||||
|
TENANT_SIGN_UP_TOGGLE_EMAIL_AUTH,
|
||||||
|
TENANT_SIGN_UP_CHANGE_USER_FULL_NAME,
|
||||||
|
TENANT_SIGN_UP_CHANGE_USER_EMAIL,
|
||||||
|
TENANT_SIGN_UP_CHANGE_USER_PASSWORD,
|
||||||
|
TENANT_SIGN_UP_CHANGE_USER_PASSWORD_CONFIRMATION,
|
||||||
|
TENANT_SIGN_UP_CONFIRM_USER_FORM,
|
||||||
|
TENANT_SIGN_UP_CHANGE_TENANT_SITE_NAME,
|
||||||
|
TENANT_SIGN_UP_CHANGE_TENANT_SUBDOMAIN,
|
||||||
|
} from '../actions/Tenant/tenantSignUpFormActions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TenantSubmitActionTypes,
|
||||||
|
TENANT_SUBMIT_START,
|
||||||
|
TENANT_SUBMIT_SUCCESS,
|
||||||
|
TENANT_SUBMIT_FAILURE,
|
||||||
|
} from '../actions/Tenant/submitTenant';
|
||||||
|
|
||||||
|
export interface TenantSignUpUserFormState {
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
passwordConfirmation: string;
|
||||||
|
passwordConfirmationError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantSignUpTenantFormState {
|
||||||
|
siteName: string;
|
||||||
|
subdomain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantSignUpState {
|
||||||
|
currentStep: number;
|
||||||
|
emailAuth: boolean;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
error: string;
|
||||||
|
|
||||||
|
userForm: TenantSignUpUserFormState;
|
||||||
|
tenantForm: TenantSignUpTenantFormState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: TenantSignUpState = {
|
||||||
|
currentStep: 1,
|
||||||
|
emailAuth: false,
|
||||||
|
isSubmitting: false,
|
||||||
|
error: '',
|
||||||
|
|
||||||
|
userForm: {
|
||||||
|
fullName: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
passwordConfirmation: '',
|
||||||
|
passwordConfirmationError: false,
|
||||||
|
},
|
||||||
|
tenantForm: {
|
||||||
|
siteName: '',
|
||||||
|
subdomain: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tenantSignUpReducer = (
|
||||||
|
state = initialState,
|
||||||
|
action: TenantSignUpFormActions | TenantSubmitActionTypes,
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case TENANT_SIGN_UP_TOGGLE_EMAIL_AUTH:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
emailAuth: !state.emailAuth,
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SIGN_UP_CHANGE_USER_FULL_NAME:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
userForm: { ...state.userForm, fullName: action.fullName },
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SIGN_UP_CHANGE_USER_EMAIL:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
userForm: { ...state.userForm, email: action.email },
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SIGN_UP_CHANGE_USER_PASSWORD:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
userForm: { ...state.userForm, password: action.password },
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SIGN_UP_CHANGE_USER_PASSWORD_CONFIRMATION:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
userForm: {
|
||||||
|
...state.userForm,
|
||||||
|
passwordConfirmation: action.passwordConfirmation,
|
||||||
|
passwordConfirmationError: state.userForm.password !== action.passwordConfirmation,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SIGN_UP_CONFIRM_USER_FORM:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentStep: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SIGN_UP_CHANGE_TENANT_SITE_NAME:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tenantForm: { ...state.tenantForm, siteName: action.siteName },
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SIGN_UP_CHANGE_TENANT_SUBDOMAIN:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tenantForm: { ...state.tenantForm, subdomain: action.subdomain },
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SUBMIT_START:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isSubmitting: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SUBMIT_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentStep: 3,
|
||||||
|
isSubmitting: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
case TENANT_SUBMIT_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isSubmitting: false,
|
||||||
|
error: action.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tenantSignUpReducer;
|
||||||
@@ -36,6 +36,25 @@
|
|||||||
a { color: $primary-color; }
|
a { color: $primary-color; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formRow {
|
||||||
|
@extend .form-row;
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
@extend .form-group;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formControl {
|
||||||
|
@extend .form-control;
|
||||||
|
|
||||||
|
&.invalid {
|
||||||
|
border-color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.switch {
|
.switch {
|
||||||
@extend
|
@extend
|
||||||
.custom-control-input;
|
.custom-control-input;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
.align-top,
|
.align-top,
|
||||||
.mr-2;
|
.mr-2;
|
||||||
|
|
||||||
width: 36px;
|
height: 36px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,11 @@
|
|||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.smallContainer {
|
||||||
|
max-width: 540px;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
.turbolinks-progress-bar {
|
.turbolinks-progress-bar {
|
||||||
background-color: $primary-color;
|
background-color: $primary-color;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
.postStatusForm {
|
.postStatusForm {
|
||||||
@extend
|
@extend
|
||||||
.d-flex,
|
.d-flex,
|
||||||
|
.flex-grow-1,
|
||||||
.m-2;
|
.m-2;
|
||||||
|
|
||||||
column-gap: 8px;
|
column-gap: 8px;
|
||||||
|
|||||||
@@ -4,4 +4,8 @@
|
|||||||
.error {
|
.error {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: #fd7e14;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
3
app/javascript/stylesheets/components/TenantSignUp.scss
Normal file
3
app/javascript/stylesheets/components/TenantSignUp.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.tenantSignUpContainer {
|
||||||
|
@extend .smallContainer;
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
@import 'common/scroll_shadows';
|
@import 'common/scroll_shadows';
|
||||||
|
|
||||||
/* Components */
|
/* Components */
|
||||||
|
@import 'components/TenantSignUp';
|
||||||
@import 'components/Board';
|
@import 'components/Board';
|
||||||
@import 'components/Comments';
|
@import 'components/Comments';
|
||||||
@import 'components/LikeButton';
|
@import 'components/LikeButton';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
default from: "notifications@example.com"
|
default from: "notifications@astuto.io"
|
||||||
layout 'mailer'
|
layout 'mailer'
|
||||||
|
helper :application
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ class UserMailer < ApplicationMailer
|
|||||||
layout 'user_mailer'
|
layout 'user_mailer'
|
||||||
|
|
||||||
def notify_post_owner(comment:)
|
def notify_post_owner(comment:)
|
||||||
|
@tenant = comment.tenant
|
||||||
|
Current.tenant = @tenant
|
||||||
@comment = comment
|
@comment = comment
|
||||||
@user = comment.post.user
|
@user = comment.post.user
|
||||||
|
|
||||||
@@ -12,6 +14,8 @@ class UserMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notify_comment_owner(comment:)
|
def notify_comment_owner(comment:)
|
||||||
|
@tenant = comment.tenant
|
||||||
|
Current.tenant = @tenant
|
||||||
@comment = comment
|
@comment = comment
|
||||||
@user = comment.parent.user
|
@user = comment.parent.user
|
||||||
|
|
||||||
@@ -22,6 +26,8 @@ class UserMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notify_followers_of_post_update(comment:)
|
def notify_followers_of_post_update(comment:)
|
||||||
|
@tenant = comment.tenant
|
||||||
|
Current.tenant = @tenant
|
||||||
@comment = comment
|
@comment = comment
|
||||||
|
|
||||||
mail(
|
mail(
|
||||||
@@ -31,6 +37,8 @@ class UserMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notify_followers_of_post_status_change(post:)
|
def notify_followers_of_post_status_change(post:)
|
||||||
|
@tenant = post.tenant
|
||||||
|
Current.tenant = @tenant
|
||||||
@post = post
|
@post = post
|
||||||
|
|
||||||
mail(
|
mail(
|
||||||
@@ -42,6 +50,6 @@ class UserMailer < ApplicationMailer
|
|||||||
private
|
private
|
||||||
|
|
||||||
def app_name
|
def app_name
|
||||||
ENV.fetch('APP_NAME')
|
Current.tenant_or_raise!.site_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
class Board < ApplicationRecord
|
class Board < ApplicationRecord
|
||||||
|
include TenantOwnable
|
||||||
include Orderable
|
include Orderable
|
||||||
|
|
||||||
has_many :posts, dependent: :destroy
|
has_many :posts, dependent: :destroy
|
||||||
|
|
||||||
validates :name, presence: true, uniqueness: true
|
validates :name, presence: true, uniqueness: { scope: :tenant_id }
|
||||||
validates :description, length: { in: 0..1024 }, allow_nil: true
|
validates :description, length: { in: 0..1024 }, allow_nil: true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class Comment < ApplicationRecord
|
class Comment < ApplicationRecord
|
||||||
|
include TenantOwnable
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :post
|
belongs_to :post
|
||||||
belongs_to :parent, class_name: 'Comment', optional: true
|
belongs_to :parent, class_name: 'Comment', optional: true
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ module Orderable
|
|||||||
def set_order_to_last
|
def set_order_to_last
|
||||||
return unless new_record?
|
return unless new_record?
|
||||||
return unless order.nil?
|
return unless order.nil?
|
||||||
|
|
||||||
order_last = self.class.maximum(:order) || -1
|
order_last = self.class.maximum(:order) || -1
|
||||||
self.order = order_last + 1
|
self.order = order_last + 1
|
||||||
end
|
end
|
||||||
|
|||||||
16
app/models/concerns/TenantOwnable.rb
Normal file
16
app/models/concerns/TenantOwnable.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# A TenantOwnable model belongs to a tenant
|
||||||
|
# A TenantOwnable model must have a tenant_id column
|
||||||
|
|
||||||
|
module TenantOwnable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
# Tenant is actually not optional, but we not do want
|
||||||
|
# to generate a SELECT query to verify the tenant is
|
||||||
|
# there every time. We get this protection for free
|
||||||
|
# through database FK constraints
|
||||||
|
belongs_to :tenant, optional: true
|
||||||
|
|
||||||
|
default_scope { where(tenant: Current.tenant_or_raise!) }
|
||||||
|
end
|
||||||
|
end
|
||||||
13
app/models/current.rb
Normal file
13
app/models/current.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
|
attribute :tenant
|
||||||
|
|
||||||
|
class MissingCurrentTenant < StandardError; end
|
||||||
|
class CurrentTenantNotActive < StandardError; end
|
||||||
|
|
||||||
|
def tenant_or_raise!
|
||||||
|
raise MissingCurrentTenant, "Current tenant is not set" unless tenant
|
||||||
|
raise CurrentTenantBlocked, "Current tenant is blocked" unless tenant.status != "blocked"
|
||||||
|
|
||||||
|
tenant
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
class Follow < ApplicationRecord
|
class Follow < ApplicationRecord
|
||||||
|
include TenantOwnable
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :post
|
belongs_to :post
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class Like < ApplicationRecord
|
class Like < ApplicationRecord
|
||||||
|
include TenantOwnable
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :post
|
belongs_to :post
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class Post < ApplicationRecord
|
class Post < ApplicationRecord
|
||||||
|
include TenantOwnable
|
||||||
|
|
||||||
belongs_to :board
|
belongs_to :board
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :post_status, optional: true
|
belongs_to :post_status, optional: true
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
class PostStatus < ApplicationRecord
|
class PostStatus < ApplicationRecord
|
||||||
|
include TenantOwnable
|
||||||
include Orderable
|
include Orderable
|
||||||
|
|
||||||
has_many :posts, dependent: :nullify
|
has_many :posts, dependent: :nullify
|
||||||
|
|
||||||
validates :name, presence: true, uniqueness: true
|
validates :name, presence: true, uniqueness: { scope: :tenant_id }
|
||||||
validates :color, format: { with: /\A#(?:[0-9a-fA-F]{3}){1,2}\z/ }
|
validates :color, format: { with: /\A#(?:[0-9a-fA-F]{3}){1,2}\z/ }
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class PostStatusChange < ApplicationRecord
|
class PostStatusChange < ApplicationRecord
|
||||||
|
include TenantOwnable
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :post
|
belongs_to :post
|
||||||
belongs_to :post_status, optional: true
|
belongs_to :post_status, optional: true
|
||||||
|
|||||||
24
app/models/tenant.rb
Normal file
24
app/models/tenant.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
class Tenant < ApplicationRecord
|
||||||
|
has_many :boards
|
||||||
|
has_many :post_statuses
|
||||||
|
has_many :posts
|
||||||
|
has_many :users
|
||||||
|
|
||||||
|
enum brand_display_setting: [
|
||||||
|
:name_and_logo,
|
||||||
|
:name_only,
|
||||||
|
:logo_only,
|
||||||
|
:no_name_no_logo
|
||||||
|
]
|
||||||
|
|
||||||
|
enum status: [:active, :pending, :blocked]
|
||||||
|
|
||||||
|
after_initialize :set_default_status, if: :new_record?
|
||||||
|
|
||||||
|
validates :site_name, presence: true
|
||||||
|
validates :subdomain, presence: true, uniqueness: true
|
||||||
|
|
||||||
|
def set_default_status
|
||||||
|
self.status ||= :pending
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
include TenantOwnable
|
||||||
|
|
||||||
devise :database_authenticatable, :registerable,
|
devise :database_authenticatable, :registerable,
|
||||||
:recoverable, :rememberable, :validatable,
|
:recoverable, :rememberable, :confirmable
|
||||||
:confirmable
|
|
||||||
|
|
||||||
has_many :posts, dependent: :destroy
|
has_many :posts, dependent: :destroy
|
||||||
has_many :likes, dependent: :destroy
|
has_many :likes, dependent: :destroy
|
||||||
@@ -16,6 +17,12 @@ class User < ApplicationRecord
|
|||||||
before_save :skip_confirmation
|
before_save :skip_confirmation
|
||||||
|
|
||||||
validates :full_name, presence: true, length: { in: 2..32 }
|
validates :full_name, presence: true, length: { in: 2..32 }
|
||||||
|
validates :email,
|
||||||
|
presence: true,
|
||||||
|
uniqueness: { scope: :tenant_id, case_sensitive: false },
|
||||||
|
format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||||
|
validates :password, allow_blank: true, length: { in: 6..128 }
|
||||||
|
validates :password, presence: true, on: :create
|
||||||
|
|
||||||
def set_default_role
|
def set_default_role
|
||||||
self.role ||= :user
|
self.role ||= :user
|
||||||
@@ -33,6 +40,17 @@ class User < ApplicationRecord
|
|||||||
active? ? super : :blocked_or_deleted
|
active? ? super : :blocked_or_deleted
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Override Devise::Confirmable#after_confirmation
|
||||||
|
# Used to change tenant status from pending to active on owner email confirmation
|
||||||
|
def after_confirmation
|
||||||
|
tenant = self.tenant
|
||||||
|
|
||||||
|
if tenant.status == "pending" and tenant.users.count == 1
|
||||||
|
tenant.status = "active"
|
||||||
|
tenant.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def skip_confirmation
|
def skip_confirmation
|
||||||
return if Rails.application.email_confirmation?
|
return if Rails.application.email_confirmation?
|
||||||
skip_confirmation!
|
skip_confirmation!
|
||||||
|
|||||||
21
app/policies/tenant_policy.rb
Normal file
21
app/policies/tenant_policy.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class TenantPolicy < ApplicationPolicy
|
||||||
|
def permitted_attributes_for_create
|
||||||
|
[:site_name, :subdomain]
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_attributes_for_update
|
||||||
|
if user.admin?
|
||||||
|
[:site_name, :site_logo, :brand_display_setting, :locale]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
user.admin? and user.tenant_id == record.id
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<p>Welcome <%= @email %>!</p>
|
<p><%= t('mailers.devise.welcome_greeting', { email: @email, site_name: Current.tenant_or_raise!.site_name }) %></p>
|
||||||
|
|
||||||
<p>You can confirm your account email through the link below:</p>
|
<p><%= t('mailers.devise.confirmation_instructions.body') %></p>
|
||||||
|
|
||||||
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
|
<p>
|
||||||
|
<%= link_to t('mailers.devise.confirmation_instructions.action'),
|
||||||
|
add_subdomain_to(method(:confirmation_url), @resource, { confirmation_token: @token })
|
||||||
|
%>
|
||||||
|
</p>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<p>Hello <%= @email %>!</p>
|
<p><%= t('mailers.devise.opening_greeting', { email: @email }) %></p>
|
||||||
|
|
||||||
<% if @resource.try(:unconfirmed_email?) %>
|
<% if @resource.try(:unconfirmed_email?) %>
|
||||||
<p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
|
<p><%= t('mailers.devise.email_changed.body', { email: @resource.unconfirmed_email }) %></p>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
|
<p><%= t('mailers.devise.email_changed.body', { email: @resource.email }) %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<p>Hello <%= @resource.email %>!</p>
|
<p><%= t('mailers.devise.opening_greeting', { email: @resource.email }) %></p>
|
||||||
|
|
||||||
<p>We're contacting you to notify you that your password has been changed.</p>
|
<p><%= t('mailers.devise.password_change.body') %></p>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<p>Hello <%= @resource.email %>!</p>
|
<p><%= t('mailers.devise.opening_greeting', { email: @resource.email }) %></p>
|
||||||
|
|
||||||
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
|
<p><%= t('mailers.devise.reset_password.body') %></p>
|
||||||
|
|
||||||
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
|
<p>
|
||||||
|
<%= link_to t('mailers.devise.reset_password.action'),
|
||||||
|
add_subdomain_to(method(:edit_password_url), @resource, { reset_password_token: @token })
|
||||||
|
%>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>If you didn't request this, please ignore this email.</p>
|
<p><%= t('mailers.devise.reset_password.body2') %></p>
|
||||||
<p>Your password won't change until you access the link above and create a new one.</p>
|
<p><%= t('mailers.devise.reset_password.body3') %></p>
|
||||||
|
|||||||
@@ -4,4 +4,8 @@
|
|||||||
|
|
||||||
<p>Click the link below to unlock your account:</p>
|
<p>Click the link below to unlock your account:</p>
|
||||||
|
|
||||||
<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>
|
<p>
|
||||||
|
<%= link_to 'Unlock my account',
|
||||||
|
add_subdomain_to(method(:unlock_url), @resource, { unlock_token: @token })
|
||||||
|
%>
|
||||||
|
</p>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<% if devise_mapping.rememberable? %>
|
<% if devise_mapping.rememberable? %>
|
||||||
<div class="form-group form-check">
|
<div class="form-group form-check">
|
||||||
<%= f.check_box :remember_me, class: "form-check-input" %>
|
<%= f.check_box :remember_me, class: "form-check-input" %>
|
||||||
<%= f.label t('common.forms.auth.remember_me'), class: "form-check-label" %>
|
<%= f.label :remember_me, t('common.forms.auth.remember_me'), class: "form-check-label" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<%=
|
<%=
|
||||||
link_to root_path, class: 'brand' do
|
link_to root_path, class: 'brand' do
|
||||||
app_name = content_tag :span, Rails.application.name
|
app_name = content_tag :span, @tenant.site_name
|
||||||
logo = image_tag(asset_pack_path('media/images/logo.png'), class: 'logo')
|
logo = image_tag(@tenant.site_logo ? @tenant.site_logo : "", class: 'logo')
|
||||||
|
|
||||||
Rails.application.show_logo? ? logo + app_name : app_name
|
if @tenant.brand_display_setting == "name_and_logo"
|
||||||
|
logo + app_name
|
||||||
|
elsif @tenant.brand_display_setting == "name_only"
|
||||||
|
app_name
|
||||||
|
elsif @tenant.brand_display_setting == "logo_only"
|
||||||
|
logo
|
||||||
|
end
|
||||||
end
|
end
|
||||||
%>
|
%>
|
||||||
|
|
||||||
@@ -37,7 +43,7 @@
|
|||||||
<% if current_user.power_user? %>
|
<% if current_user.power_user? %>
|
||||||
<%=
|
<%=
|
||||||
link_to t('header.menu.site_settings'),
|
link_to t('header.menu.site_settings'),
|
||||||
current_user.admin? ? site_settings_boards_path : site_settings_users_path,
|
current_user.admin? ? site_settings_general_path : site_settings_users_path,
|
||||||
class: 'dropdown-item'
|
class: 'dropdown-item'
|
||||||
%>
|
%>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title><%= Rails.application.name %></title>
|
<title><%= @tenant ? @tenant.site_name : @page_title %></title>
|
||||||
|
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
|
|
||||||
@@ -17,7 +18,10 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<%= render 'layouts/header' %>
|
<% if @tenant %>
|
||||||
|
<%= render 'layouts/header' %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<%= render 'layouts/alerts' %>
|
<%= render 'layouts/alerts' %>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
@@ -4,14 +4,19 @@
|
|||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p><%= t('user_mailer.opening_greeting') %></p>
|
<p><%= t('mailers.user.opening_greeting') %></p>
|
||||||
|
|
||||||
<div><%= yield %></div>
|
<div><%= yield %></div>
|
||||||
|
|
||||||
<p><%= t('user_mailer.closing_greeting') %></p>
|
<p><%= t('mailers.user.closing_greeting') %></p>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<%= link_to(t('user_mailer.unsubscribe'), edit_user_registration_url) %>.
|
<%=
|
||||||
|
link_to(
|
||||||
|
t('mailers.user.unsubscribe'),
|
||||||
|
add_subdomain_to(method(:edit_user_registration_url))
|
||||||
|
)
|
||||||
|
%>.
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
|
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
|
||||||
<% if current_user.admin? %>
|
<% if current_user.admin? %>
|
||||||
|
<%= render 'menu_link', label: t('site_settings.menu.general'), path: site_settings_general_path %>
|
||||||
<%= render 'menu_link', label: t('site_settings.menu.boards'), path: site_settings_boards_path %>
|
<%= render 'menu_link', label: t('site_settings.menu.boards'), path: site_settings_boards_path %>
|
||||||
<%= render 'menu_link', label: t('site_settings.menu.post_statuses'), path: site_settings_post_statuses_path %>
|
<%= render 'menu_link', label: t('site_settings.menu.post_statuses'), path: site_settings_post_statuses_path %>
|
||||||
<%= render 'menu_link', label: t('site_settings.menu.roadmap'), path: site_settings_roadmap_path %>
|
<%= render 'menu_link', label: t('site_settings.menu.roadmap'), path: site_settings_roadmap_path %>
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
<div class="twoColumnsContainer">
|
<div class="twoColumnsContainer">
|
||||||
<%= render 'menu' %>
|
<%= render 'menu' %>
|
||||||
|
<div>
|
||||||
<div class="content">
|
<%=
|
||||||
<h2>General</h2>
|
react_component(
|
||||||
<p>Under construction</p>
|
'SiteSettings/General',
|
||||||
|
{
|
||||||
|
originForm: {
|
||||||
|
siteName: @tenant.site_name,
|
||||||
|
siteLogo: @tenant.site_logo,
|
||||||
|
brandDisplaySetting: @tenant.brand_display_setting,
|
||||||
|
locale: @tenant.locale
|
||||||
|
},
|
||||||
|
authenticityToken: form_authenticity_token
|
||||||
|
}
|
||||||
|
)
|
||||||
|
%>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
5
app/views/static_pages/blocked_tenant.html.erb
Normal file
5
app/views/static_pages/blocked_tenant.html.erb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<div class="smallContainer">
|
||||||
|
<div class="box">
|
||||||
|
<h3><%= t('blocked_tenant.title') %></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
6
app/views/static_pages/pending_tenant.html.erb
Normal file
6
app/views/static_pages/pending_tenant.html.erb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<div class="smallContainer">
|
||||||
|
<div class="box">
|
||||||
|
<h3><%= t('pending_tenant.title') %></h3>
|
||||||
|
<p><%= t('pending_tenant.message') %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
8
app/views/tenants/new.html.erb
Normal file
8
app/views/tenants/new.html.erb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<%=
|
||||||
|
react_component(
|
||||||
|
'TenantSignUp',
|
||||||
|
{
|
||||||
|
authenticityToken: form_authenticity_token
|
||||||
|
}
|
||||||
|
)
|
||||||
|
%>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<p>
|
<p>
|
||||||
<%= t('user_mailer.notify_comment_owner.body', { user: @comment.user.full_name, post: @comment.post.title }) %>
|
<%= t('mailers.user.notify_comment_owner.body', { user: @comment.user.full_name, post: @comment.post.title }) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@@ -7,5 +7,5 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= link_to t('user_mailer.learn_more'), post_url(@comment.post) %>
|
<%= link_to t('mailers.user.learn_more'), add_subdomain_to(method(:post_url), @comment.post) %>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<p>
|
|
||||||
<%= I18n.t('user_mailer.notify_followers_of_post_status_change.body', { post: @post }) %>
|
|
||||||
<span style='background-color: <%= @post.post_status.color %>; color: white;'%>>
|
|
||||||
<%= @post.post_status.name %>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<%= link_to I18n.t('user_mailer.learn_more'), post_url(@post) %>
|
|
||||||
</p>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<p>
|
||||||
|
<%= t('mailers.user.notify_followers_of_post_status_change.body', { post: @post }) %>
|
||||||
|
<span style='background-color: <%= @post.post_status.color %>; color: white;'%>>
|
||||||
|
<%= @post.post_status.name %>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= link_to t('mailers.user.learn_more'), add_subdomain_to(method(:post_url), @post) %>
|
||||||
|
</p>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<p>
|
|
||||||
<%= I18n.t('user_mailer.notify_followers_of_post_update.body', { user: @comment.user.full_name, post: @comment.post.title }) %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<i><%= @comment.body %></i>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<%= link_to I18n.t('user_mailer.learn_more'), post_url(@comment.post) %>
|
|
||||||
</p>
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<p>
|
||||||
|
<%= t('mailers.user.notify_followers_of_post_update.body', { user: @comment.user.full_name, post: @comment.post.title }) %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<i><%= @comment.body %></i>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= link_to t('mailers.user.learn_more'), add_subdomain_to(method(:post_url), @comment.post) %>
|
||||||
|
</p>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<p>
|
<p>
|
||||||
<%= t('user_mailer.notify_post_owner.body', { user: @comment.user.full_name, post: @comment.post.title }) %>
|
<%= t('mailers.user.notify_post_owner.body', { user: @comment.user.full_name, post: @comment.post.title }) %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@@ -7,5 +7,5 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<%= link_to t('user_mailer.learn_more'), post_url(@comment.post) %>
|
<%= link_to t('mailers.user.learn_more'), add_subdomain_to(method(:post_url), @comment.post) %>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ env_vars=(
|
|||||||
"POSTGRES_USER" \
|
"POSTGRES_USER" \
|
||||||
"POSTGRES_PASSWORD" \
|
"POSTGRES_PASSWORD" \
|
||||||
"EMAIL_CONFIRMATION" \
|
"EMAIL_CONFIRMATION" \
|
||||||
"APP_NAME" \
|
|
||||||
"SHOW_LOGO" \
|
|
||||||
"POSTS_PER_PAGE" \
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check each one
|
# Check each one
|
||||||
|
|||||||
@@ -16,20 +16,16 @@ module App
|
|||||||
# -- all .rb files in that directory are automatically loaded after loading
|
# -- all .rb files in that directory are automatically loaded after loading
|
||||||
# the framework and any gems in your application.
|
# the framework and any gems in your application.
|
||||||
|
|
||||||
def name
|
def multi_tenancy?
|
||||||
ENV["APP_NAME"]
|
ENV["MULTI_TENANCY"] == "true"
|
||||||
end
|
end
|
||||||
|
|
||||||
def email_confirmation?
|
def email_confirmation?
|
||||||
ENV["EMAIL_CONFIRMATION"] == "yes"
|
ENV["EMAIL_CONFIRMATION"] == "true"
|
||||||
end
|
|
||||||
|
|
||||||
def show_logo?
|
|
||||||
ENV["SHOW_LOGO"] == "yes"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def posts_per_page
|
def posts_per_page
|
||||||
ENV["POSTS_PER_PAGE"].to_i
|
15
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Settings specified here will take precedence over those in config/application.rb.
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|
||||||
|
# For subdomains in localhost
|
||||||
|
config.action_dispatch.tld_length = 0
|
||||||
|
|
||||||
# For Devise
|
# For Devise
|
||||||
config.action_mailer.default_url_options = { host: 'localhost:3000' }
|
config.action_mailer.default_url_options = { host: 'localhost:3000' }
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
# Set up default environment variables
|
# Set up default environment variables
|
||||||
ENV["EMAIL_CONFIRMATION"] = "no"
|
ENV["EMAIL_CONFIRMATION"] = "no"
|
||||||
ENV["POSTS_PER_PAGE"] = "15"
|
|
||||||
|
|
||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Settings specified here will take precedence over those in config/application.rb.
|
# Settings specified here will take precedence over those in config/application.rb.
|
||||||
|
|||||||
@@ -12,19 +12,19 @@ Devise.setup do |config|
|
|||||||
|
|
||||||
# ==> Controller configuration
|
# ==> Controller configuration
|
||||||
# Configure the parent class to the devise controllers.
|
# Configure the parent class to the devise controllers.
|
||||||
# config.parent_controller = 'DeviseController'
|
# config.parent_controller = 'ApplicationController'
|
||||||
|
|
||||||
# ==> Mailer Configuration
|
# ==> Mailer Configuration
|
||||||
# Configure the e-mail address which will be shown in Devise::Mailer,
|
# Configure the e-mail address which will be shown in Devise::Mailer,
|
||||||
# note that it will be overwritten if you use your own mailer class
|
# note that it will be overwritten if you use your own mailer class
|
||||||
# with default "from" parameter.
|
# with default "from" parameter.
|
||||||
config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
|
config.mailer_sender = 'notifications@astuto.io'
|
||||||
|
|
||||||
# Configure the class responsible to send e-mails.
|
# Configure the class responsible to send e-mails.
|
||||||
# config.mailer = 'Devise::Mailer'
|
# config.mailer = 'Devise::Mailer'
|
||||||
|
|
||||||
# Configure the parent class responsible to send e-mails.
|
# Configure the parent class responsible to send e-mails.
|
||||||
# config.parent_mailer = 'ActionMailer::Base'
|
config.parent_mailer = 'ApplicationMailer'
|
||||||
|
|
||||||
# ==> ORM configuration
|
# ==> ORM configuration
|
||||||
# Load and configure the ORM. Supports :active_record (default) and
|
# Load and configure the ORM. Supports :active_record (default) and
|
||||||
@@ -166,12 +166,12 @@ Devise.setup do |config|
|
|||||||
|
|
||||||
# ==> Configuration for :validatable
|
# ==> Configuration for :validatable
|
||||||
# Range for password length.
|
# Range for password length.
|
||||||
config.password_length = 6..128
|
# config.password_length = 6..128
|
||||||
|
|
||||||
# Email regex used to validate email formats. It simply asserts that
|
# Email regex used to validate email formats. It simply asserts that
|
||||||
# one (and only one) @ exists in the given string. This is mainly
|
# one (and only one) @ exists in the given string. This is mainly
|
||||||
# to give user feedback and not to assert the e-mail validity.
|
# to give user feedback and not to assert the e-mail validity.
|
||||||
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
|
# config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
|
||||||
|
|
||||||
# ==> Configuration for :timeoutable
|
# ==> Configuration for :timeoutable
|
||||||
# The time you want to timeout the user session without activity. After this
|
# The time you want to timeout the user session without activity. After this
|
||||||
|
|||||||
6
config/initializers/reserved_subdomains.rb
Normal file
6
config/initializers/reserved_subdomains.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
RESERVED_SUBDOMAINS = [
|
||||||
|
'showcase',
|
||||||
|
'login',
|
||||||
|
'help',
|
||||||
|
'playground'
|
||||||
|
]
|
||||||
@@ -2,7 +2,7 @@ en:
|
|||||||
common:
|
common:
|
||||||
forms:
|
forms:
|
||||||
auth:
|
auth:
|
||||||
email: 'Email address'
|
email: 'Email'
|
||||||
full_name: 'Full name'
|
full_name: 'Full name'
|
||||||
password: 'Password'
|
password: 'Password'
|
||||||
password_confirmation: 'Password confirmation'
|
password_confirmation: 'Password confirmation'
|
||||||
@@ -38,6 +38,7 @@ en:
|
|||||||
cancel: 'Cancel'
|
cancel: 'Cancel'
|
||||||
create: 'Create'
|
create: 'Create'
|
||||||
update: 'Save'
|
update: 'Save'
|
||||||
|
confirm: 'Confirm'
|
||||||
datetime:
|
datetime:
|
||||||
now: 'just now'
|
now: 'just now'
|
||||||
minutes:
|
minutes:
|
||||||
@@ -49,6 +50,19 @@ en:
|
|||||||
days:
|
days:
|
||||||
one: '1 day ago'
|
one: '1 day ago'
|
||||||
other: '%{count} days ago'
|
other: '%{count} days ago'
|
||||||
|
signup:
|
||||||
|
page_title: 'Create your feedback space'
|
||||||
|
step1:
|
||||||
|
title: '1. Create user account'
|
||||||
|
email_auth: 'Register with email'
|
||||||
|
step2:
|
||||||
|
title: '2. Create feedback space'
|
||||||
|
site_name: 'Site name'
|
||||||
|
subdomain: 'Subdomain'
|
||||||
|
create_button: 'Create feedback space'
|
||||||
|
step3:
|
||||||
|
title: "You're almost done!"
|
||||||
|
message: "Check your email %{email} to activate your new feedback space %{subdomain}!"
|
||||||
header:
|
header:
|
||||||
menu:
|
menu:
|
||||||
site_settings: 'Site settings'
|
site_settings: 'Site settings'
|
||||||
@@ -57,6 +71,11 @@ en:
|
|||||||
log_in: 'Log in / Sign up'
|
log_in: 'Log in / Sign up'
|
||||||
roadmap:
|
roadmap:
|
||||||
title: 'Roadmap'
|
title: 'Roadmap'
|
||||||
|
pending_tenant:
|
||||||
|
title: 'Verify your email address'
|
||||||
|
message: 'We''ve sent an email with an activation link to the email you provided during registration. Click on that link to activate this feedback space!'
|
||||||
|
blocked_tenant:
|
||||||
|
title: 'This feedback space has been blocked'
|
||||||
board:
|
board:
|
||||||
new_post:
|
new_post:
|
||||||
submit_button: 'Submit feedback'
|
submit_button: 'Submit feedback'
|
||||||
@@ -103,6 +122,7 @@ en:
|
|||||||
site_settings:
|
site_settings:
|
||||||
menu:
|
menu:
|
||||||
title: 'Site settings'
|
title: 'Site settings'
|
||||||
|
general: 'General'
|
||||||
boards: 'Boards'
|
boards: 'Boards'
|
||||||
post_statuses: 'Statuses'
|
post_statuses: 'Statuses'
|
||||||
roadmap: 'Roadmap'
|
roadmap: 'Roadmap'
|
||||||
@@ -110,6 +130,17 @@ en:
|
|||||||
info_box:
|
info_box:
|
||||||
up_to_date: 'All changes saved'
|
up_to_date: 'All changes saved'
|
||||||
error: 'An error occurred: %{message}'
|
error: 'An error occurred: %{message}'
|
||||||
|
dirty: 'Changes not saved'
|
||||||
|
general:
|
||||||
|
title: 'General'
|
||||||
|
site_name: 'Site name'
|
||||||
|
site_logo: 'Site logo'
|
||||||
|
brand_setting: 'Display'
|
||||||
|
brand_setting_both: 'Both name and logo'
|
||||||
|
brand_setting_name: 'Name only'
|
||||||
|
brand_setting_logo: 'Logo only'
|
||||||
|
brand_setting_none: 'None'
|
||||||
|
locale: 'Language'
|
||||||
boards:
|
boards:
|
||||||
title: 'Boards'
|
title: 'Boards'
|
||||||
empty: 'There are no boards. Create one below!'
|
empty: 'There are no boards. Create one below!'
|
||||||
@@ -127,7 +158,7 @@ en:
|
|||||||
title: 'Roadmap'
|
title: 'Roadmap'
|
||||||
title2: 'Not in roadmap'
|
title2: 'Not in roadmap'
|
||||||
empty: 'The roadmap is empty.'
|
empty: 'The roadmap is empty.'
|
||||||
help: 'You can add statuses to the roadmap by dragging them from the section below. If you instead want to create a new status or change their order, go to Site settings -> Statuses.'
|
help: 'You can add statuses to the roadmap by dragging them from the section below. If you instead want to create a new status or change their order, go to Site settings > Statuses.'
|
||||||
users:
|
users:
|
||||||
title: 'Users'
|
title: 'Users'
|
||||||
block: 'Block'
|
block: 'Block'
|
||||||
@@ -141,23 +172,40 @@ en:
|
|||||||
status_active: 'Active'
|
status_active: 'Active'
|
||||||
status_blocked: 'Blocked'
|
status_blocked: 'Blocked'
|
||||||
status_deleted: 'Deleted'
|
status_deleted: 'Deleted'
|
||||||
user_mailer:
|
mailers:
|
||||||
opening_greeting: 'Hello!'
|
devise:
|
||||||
closing_greeting: 'Have a great day!'
|
welcome_greeting: 'Welcome to %{site_name}, %{email}!'
|
||||||
learn_more: 'Click here to learn more'
|
opening_greeting: 'Hello %{email}!'
|
||||||
unsubscribe: 'Annoyed? You can turn off notifications here'
|
confirmation_instructions:
|
||||||
notify_post_owner:
|
body: 'You can confirm your account email through the link below:'
|
||||||
subject: '[%{app_name}] New comment on %{post}'
|
action: 'Confirm my account'
|
||||||
body: 'There is a new comment by %{user} on your post %{post}'
|
email_changed:
|
||||||
notify_comment_owner:
|
body: "We're contacting you to notify you that your email is being changed to %{email}."
|
||||||
subject: '[%{app_name}] New reply on your comment from %{post}'
|
body2: "We're contacting you to notify you that your email has been changed to %{email}."
|
||||||
body: 'There is a new reply by %{user} on your comment from post %{post}'
|
password_change:
|
||||||
notify_followers_of_post_update:
|
body: "We're contacting you to notify you that your password has been changed."
|
||||||
subject: '[%{app_name}] New update for post %{post}'
|
reset_password:
|
||||||
body: "There is a new update on the post you're following %{post}"
|
body: 'Someone has requested a link to change your password. You can do this through the link below.'
|
||||||
notify_followers_of_post_status_change:
|
body2: "If you didn't request this, please ignore this email."
|
||||||
subject: '[%{app_name}] Status change on post %{post}'
|
body3: "Your password won't change until you access the link above and create a new one."
|
||||||
body: "The post you're following %{post} has a new status"
|
action: 'Change my password'
|
||||||
|
user:
|
||||||
|
opening_greeting: 'Hello!'
|
||||||
|
closing_greeting: 'Have a great day!'
|
||||||
|
learn_more: 'Click here to learn more'
|
||||||
|
unsubscribe: 'Annoyed? You can turn off notifications here'
|
||||||
|
notify_post_owner:
|
||||||
|
subject: '[%{app_name}] New comment on %{post}'
|
||||||
|
body: 'There is a new comment by %{user} on your post %{post}'
|
||||||
|
notify_comment_owner:
|
||||||
|
subject: '[%{app_name}] New reply on your comment from %{post}'
|
||||||
|
body: 'There is a new reply by %{user} on your comment from post %{post}'
|
||||||
|
notify_followers_of_post_update:
|
||||||
|
subject: '[%{app_name}] New update for post %{post}'
|
||||||
|
body: "There is a new update on the post you're following %{post}"
|
||||||
|
notify_followers_of_post_status_change:
|
||||||
|
subject: '[%{app_name}] Status change on post %{post}'
|
||||||
|
body: "The post you're following %{post} has a new status"
|
||||||
backend:
|
backend:
|
||||||
errors:
|
errors:
|
||||||
unauthorized: 'You are not authorized'
|
unauthorized: 'You are not authorized'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ it:
|
|||||||
common:
|
common:
|
||||||
forms:
|
forms:
|
||||||
auth:
|
auth:
|
||||||
email: 'Indirizzo email'
|
email: 'Email'
|
||||||
full_name: 'Nome e cognome'
|
full_name: 'Nome e cognome'
|
||||||
password: 'Password'
|
password: 'Password'
|
||||||
password_confirmation: 'Conferma password'
|
password_confirmation: 'Conferma password'
|
||||||
@@ -38,6 +38,7 @@ it:
|
|||||||
cancel: 'Annulla'
|
cancel: 'Annulla'
|
||||||
create: 'Crea'
|
create: 'Crea'
|
||||||
update: 'Salva'
|
update: 'Salva'
|
||||||
|
confirm: 'Conferma'
|
||||||
datetime:
|
datetime:
|
||||||
now: 'adesso'
|
now: 'adesso'
|
||||||
minutes:
|
minutes:
|
||||||
@@ -49,6 +50,19 @@ it:
|
|||||||
days:
|
days:
|
||||||
one: '1 giorno fa'
|
one: '1 giorno fa'
|
||||||
other: '%{count} giorni fa'
|
other: '%{count} giorni fa'
|
||||||
|
signup:
|
||||||
|
page_title: 'Crea il tuo spazio di feedback'
|
||||||
|
step1:
|
||||||
|
title: '1. Crea account utente'
|
||||||
|
email_auth: 'Registrati con indirizzo email'
|
||||||
|
step2:
|
||||||
|
title: '2. Crea spazio di feedback'
|
||||||
|
site_name: 'Nome del sito'
|
||||||
|
subdomain: 'Sottodominio'
|
||||||
|
create_button: 'Crea spazio feedback'
|
||||||
|
step3:
|
||||||
|
title: 'Hai quasi finito!'
|
||||||
|
message: "Controlla la tua email %{email} per attivare il tuo nuovo spazio di feedback %{subdomain}!"
|
||||||
header:
|
header:
|
||||||
menu:
|
menu:
|
||||||
site_settings: 'Impostazioni sito'
|
site_settings: 'Impostazioni sito'
|
||||||
@@ -57,6 +71,11 @@ it:
|
|||||||
log_in: 'Accedi / Registrati'
|
log_in: 'Accedi / Registrati'
|
||||||
roadmap:
|
roadmap:
|
||||||
title: 'Roadmap'
|
title: 'Roadmap'
|
||||||
|
pending_tenant:
|
||||||
|
title: 'Verifica il tuo indirizzo email'
|
||||||
|
message: 'Abbiamo mandato una email con un link di attivazione all''indirizzo email che hai specificato in fase di registrazione. Clicca su quel link per attivare questo spazio di feedback!'
|
||||||
|
blocked_tenant:
|
||||||
|
title: 'This feedback space has been blocked'
|
||||||
board:
|
board:
|
||||||
new_post:
|
new_post:
|
||||||
submit_button: 'Invia feedback'
|
submit_button: 'Invia feedback'
|
||||||
@@ -103,6 +122,7 @@ it:
|
|||||||
site_settings:
|
site_settings:
|
||||||
menu:
|
menu:
|
||||||
title: 'Impostazioni sito'
|
title: 'Impostazioni sito'
|
||||||
|
general: 'Generali'
|
||||||
boards: 'Bacheche'
|
boards: 'Bacheche'
|
||||||
post_statuses: 'Stati'
|
post_statuses: 'Stati'
|
||||||
roadmap: 'Roadmap'
|
roadmap: 'Roadmap'
|
||||||
@@ -110,6 +130,17 @@ it:
|
|||||||
info_box:
|
info_box:
|
||||||
up_to_date: 'Tutte le modifiche sono state salvate'
|
up_to_date: 'Tutte le modifiche sono state salvate'
|
||||||
error: 'Si è verificato un errore: %{message}'
|
error: 'Si è verificato un errore: %{message}'
|
||||||
|
dirty: 'Modifiche non salvate'
|
||||||
|
general:
|
||||||
|
title: 'Generale'
|
||||||
|
site_name: 'Nome del sito'
|
||||||
|
site_logo: 'Logo del sito'
|
||||||
|
brand_setting: 'Mostra'
|
||||||
|
brand_setting_both: 'Sia nome che logo'
|
||||||
|
brand_setting_name: 'Solo nome'
|
||||||
|
brand_setting_logo: 'Solo logo'
|
||||||
|
brand_setting_none: 'Nessuno'
|
||||||
|
locale: 'Lingua'
|
||||||
boards:
|
boards:
|
||||||
title: 'Bacheche'
|
title: 'Bacheche'
|
||||||
empty: 'Non ci sono bacheche. Creane una qua sotto!'
|
empty: 'Non ci sono bacheche. Creane una qua sotto!'
|
||||||
@@ -141,23 +172,40 @@ it:
|
|||||||
status_active: 'Attivo'
|
status_active: 'Attivo'
|
||||||
status_blocked: 'Bloccato'
|
status_blocked: 'Bloccato'
|
||||||
status_deleted: 'Eliminato'
|
status_deleted: 'Eliminato'
|
||||||
user_mailer:
|
mailers:
|
||||||
opening_greeting: 'Ciao!'
|
devise:
|
||||||
closing_greeting: 'Buona giornata!'
|
welcome_greeting: 'Benvenuto su %{site_name}, %{email}!'
|
||||||
learn_more: 'Clicca qui per saperne di più'
|
opening_greeting: 'Ciao %{email}!'
|
||||||
unsubscribe: 'Non vuoi più ricevere notifiche? Clicca qui'
|
confirmation_instructions:
|
||||||
notify_post_owner:
|
body: 'Puoi confermare il tuo account cliccando il link qua sotto:'
|
||||||
subject: '[%{app_name}] Nuovo commento al tuo post %{post}'
|
action: 'Conferma il mio account'
|
||||||
body: '%{user} ha commentato il tuo post %{post}'
|
email_changed:
|
||||||
notify_comment_owner:
|
body: "Ti contattiamo per segnalarti che la tua email sta per essere modificata in %{email}."
|
||||||
subject: '[%{app_name}] Risposta al tuo commento nel post %{post}'
|
body2: "Ti contattiamo per segnalarti che la tua email è stata modificata in %{email}."
|
||||||
body: '%{user} ha risposto al tuo commento nel post %{post}'
|
password_change:
|
||||||
notify_followers_of_post_update:
|
body: "Ti contattiamo per segnalarti che la tua password è stata modificata."
|
||||||
subject: '[%{app_name}] Nuovo aggiornamento per il post %{post}'
|
reset_password:
|
||||||
body: "C'è un nuovo aggiornamento sul post che stai seguendo %{post}"
|
body: 'Qualcuno ha richiesto un link per modificare la tua password. Puoi modificare la tua password cliccando sul link qua sotto.'
|
||||||
notify_followers_of_post_status_change:
|
body2: "Se non sei stato tu a richiedere la modifica, ti preghiamo di ignorare questa email."
|
||||||
subject: '[%{app_name}] Aggiornamento stato per il post %{post}'
|
body3: "La tua password non sarà modificata finché non cliccherai sul link qua sopra e ne creerai una nuova."
|
||||||
body: "Il post che segui %{post} ha un nuovo stato"
|
action: 'Cambia la mia password'
|
||||||
|
user:
|
||||||
|
opening_greeting: 'Ciao!'
|
||||||
|
closing_greeting: 'Buona giornata!'
|
||||||
|
learn_more: 'Clicca qui per saperne di più'
|
||||||
|
unsubscribe: 'Non vuoi più ricevere notifiche? Clicca qui'
|
||||||
|
notify_post_owner:
|
||||||
|
subject: '[%{app_name}] Nuovo commento al tuo post %{post}'
|
||||||
|
body: '%{user} ha commentato il tuo post %{post}'
|
||||||
|
notify_comment_owner:
|
||||||
|
subject: '[%{app_name}] Risposta al tuo commento nel post %{post}'
|
||||||
|
body: '%{user} ha risposto al tuo commento nel post %{post}'
|
||||||
|
notify_followers_of_post_update:
|
||||||
|
subject: '[%{app_name}] Nuovo aggiornamento per il post %{post}'
|
||||||
|
body: "C'è un nuovo aggiornamento sul post che stai seguendo %{post}"
|
||||||
|
notify_followers_of_post_status_change:
|
||||||
|
subject: '[%{app_name}] Aggiornamento stato per il post %{post}'
|
||||||
|
body: "Il post che segui %{post} ha un nuovo stato"
|
||||||
backend:
|
backend:
|
||||||
errors:
|
errors:
|
||||||
unauthorized: 'Non sei autorizzato'
|
unauthorized: 'Non sei autorizzato'
|
||||||
@@ -166,4 +214,4 @@ it:
|
|||||||
board:
|
board:
|
||||||
update_order: 'Si è verificato un errore durante il riordinamento delle bacheche'
|
update_order: 'Si è verificato un errore durante il riordinamento delle bacheche'
|
||||||
post_status:
|
post_status:
|
||||||
update_order: 'Si è verificato un errore durante il riordinamento degli stati'
|
update_order: 'Si è verificato un errore durante il riordinamento degli stati'
|
||||||
@@ -1,32 +1,53 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
root to: 'static_pages#roadmap'
|
if Rails.application.multi_tenancy?
|
||||||
get '/roadmap', to: 'static_pages#roadmap'
|
constraints subdomain: 'showcase' do
|
||||||
|
root to: 'static_pages#showcase', as: :showcase
|
||||||
|
end
|
||||||
|
|
||||||
|
constraints subdomain: 'login' do
|
||||||
|
get '/signup', to: 'tenants#new'
|
||||||
|
resource :tenants, only: [:create]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
constraints subdomain: /.*/ do
|
||||||
|
root to: 'static_pages#roadmap'
|
||||||
|
|
||||||
|
get '/roadmap', to: 'static_pages#roadmap'
|
||||||
|
get '/pending-tenant', to: 'static_pages#pending_tenant'
|
||||||
|
get '/blocked-tenant', to: 'static_pages#blocked_tenant'
|
||||||
|
|
||||||
|
devise_for :users, :controllers => {
|
||||||
|
:registrations => 'registrations',
|
||||||
|
:sessions => 'sessions'
|
||||||
|
}
|
||||||
|
|
||||||
|
resources :tenants, only: [:show, :update]
|
||||||
|
resources :users, only: [:index, :update]
|
||||||
|
|
||||||
devise_for :users, :controllers => { :registrations => 'registrations' }
|
resources :posts, only: [:index, :create, :show, :update, :destroy] do
|
||||||
resources :users, only: [:index, :update]
|
resource :follows, only: [:create, :destroy]
|
||||||
|
resources :follows, only: [:index]
|
||||||
resources :posts, only: [:index, :create, :show, :update, :destroy] do
|
resource :likes, only: [:create, :destroy]
|
||||||
resource :follows, only: [:create, :destroy]
|
resources :likes, only: [:index]
|
||||||
resources :follows, only: [:index]
|
resources :comments, only: [:index, :create, :update, :destroy]
|
||||||
resource :likes, only: [:create, :destroy]
|
resources :post_status_changes, only: [:index]
|
||||||
resources :likes, only: [:index]
|
end
|
||||||
resources :comments, only: [:index, :create, :update, :destroy]
|
|
||||||
resources :post_status_changes, only: [:index]
|
resources :boards, only: [:index, :create, :update, :destroy, :show] do
|
||||||
end
|
patch 'update_order', on: :collection
|
||||||
|
end
|
||||||
|
|
||||||
resources :boards, only: [:index, :create, :update, :destroy, :show] do
|
resources :post_statuses, only: [:index, :create, :update, :destroy] do
|
||||||
patch 'update_order', on: :collection
|
patch 'update_order', on: :collection
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :post_statuses, only: [:index, :create, :update, :destroy] do
|
namespace :site_settings do
|
||||||
patch 'update_order', on: :collection
|
get 'general'
|
||||||
end
|
get 'boards'
|
||||||
|
get 'post_statuses'
|
||||||
namespace :site_settings do
|
get 'roadmap'
|
||||||
get 'general'
|
get 'users'
|
||||||
get 'boards'
|
end
|
||||||
get 'post_statuses'
|
|
||||||
get 'roadmap'
|
|
||||||
get 'users'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
13
db/migrate/20220701090736_create_tenants.rb
Normal file
13
db/migrate/20220701090736_create_tenants.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class CreateTenants < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
create_table :tenants do |t|
|
||||||
|
t.string :site_name, null: false
|
||||||
|
t.string :site_logo
|
||||||
|
t.string :subdomain, null: false, unique: true
|
||||||
|
t.string :locale, default: "en"
|
||||||
|
t.string :custom_url
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
class AddTenantForeignKeyToModels < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
# Add tenant_id fk column to all models
|
||||||
|
add_reference :boards, :tenant, foreign_key: true, null: false
|
||||||
|
add_reference :post_statuses, :tenant, foreign_key: true, null: false
|
||||||
|
add_reference :posts, :tenant, foreign_key: true, null: false
|
||||||
|
add_reference :users, :tenant, foreign_key: true, null: false
|
||||||
|
add_reference :comments, :tenant, foreign_key: true, null: false
|
||||||
|
add_reference :likes, :tenant, foreign_key: true, null: false
|
||||||
|
add_reference :follows, :tenant, foreign_key: true, null: false
|
||||||
|
add_reference :post_status_changes, :tenant, foreign_key: true, null: false
|
||||||
|
|
||||||
|
# Change index of unique columns to double index with tenant_id
|
||||||
|
remove_index :boards, :name
|
||||||
|
add_index :boards, [:name, :tenant_id], unique: true
|
||||||
|
|
||||||
|
remove_index :post_statuses, :name
|
||||||
|
add_index :post_statuses, [:name, :tenant_id], unique: true
|
||||||
|
|
||||||
|
remove_index :users, :email
|
||||||
|
add_index :users, [:email, :tenant_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddBrandDisplaySettingToTenants < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :tenants, :brand_display_setting, :integer, default: 0
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20220715092725_add_status_to_tenants.rb
Normal file
5
db/migrate/20220715092725_add_status_to_tenants.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddStatusToTenants < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :tenants, :status, :integer
|
||||||
|
end
|
||||||
|
end
|
||||||
44
db/schema.rb
44
db/schema.rb
@@ -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: 2022_06_22_092039) do
|
ActiveRecord::Schema.define(version: 2022_07_15_092725) 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"
|
||||||
@@ -21,7 +21,9 @@ ActiveRecord::Schema.define(version: 2022_06_22_092039) do
|
|||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.integer "order", null: false
|
t.integer "order", null: false
|
||||||
t.index ["name"], name: "index_boards_on_name", unique: true
|
t.bigint "tenant_id", null: false
|
||||||
|
t.index ["name", "tenant_id"], name: "index_boards_on_name_and_tenant_id", unique: true
|
||||||
|
t.index ["tenant_id"], name: "index_boards_on_tenant_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "comments", force: :cascade do |t|
|
create_table "comments", force: :cascade do |t|
|
||||||
@@ -32,8 +34,10 @@ ActiveRecord::Schema.define(version: 2022_06_22_092039) do
|
|||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.boolean "is_post_update", default: false, null: false
|
t.boolean "is_post_update", default: false, null: false
|
||||||
|
t.bigint "tenant_id", null: false
|
||||||
t.index ["parent_id"], name: "index_comments_on_parent_id"
|
t.index ["parent_id"], name: "index_comments_on_parent_id"
|
||||||
t.index ["post_id"], name: "index_comments_on_post_id"
|
t.index ["post_id"], name: "index_comments_on_post_id"
|
||||||
|
t.index ["tenant_id"], name: "index_comments_on_tenant_id"
|
||||||
t.index ["user_id"], name: "index_comments_on_user_id"
|
t.index ["user_id"], name: "index_comments_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -42,7 +46,9 @@ ActiveRecord::Schema.define(version: 2022_06_22_092039) do
|
|||||||
t.bigint "post_id", null: false
|
t.bigint "post_id", null: false
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.bigint "tenant_id", null: false
|
||||||
t.index ["post_id"], name: "index_follows_on_post_id"
|
t.index ["post_id"], name: "index_follows_on_post_id"
|
||||||
|
t.index ["tenant_id"], name: "index_follows_on_tenant_id"
|
||||||
t.index ["user_id", "post_id"], name: "index_follows_on_user_id_and_post_id", unique: true
|
t.index ["user_id", "post_id"], name: "index_follows_on_user_id_and_post_id", unique: true
|
||||||
t.index ["user_id"], name: "index_follows_on_user_id"
|
t.index ["user_id"], name: "index_follows_on_user_id"
|
||||||
end
|
end
|
||||||
@@ -52,7 +58,9 @@ ActiveRecord::Schema.define(version: 2022_06_22_092039) do
|
|||||||
t.bigint "post_id", null: false
|
t.bigint "post_id", null: false
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.bigint "tenant_id", null: false
|
||||||
t.index ["post_id"], name: "index_likes_on_post_id"
|
t.index ["post_id"], name: "index_likes_on_post_id"
|
||||||
|
t.index ["tenant_id"], name: "index_likes_on_tenant_id"
|
||||||
t.index ["user_id", "post_id"], name: "index_likes_on_user_id_and_post_id", unique: true
|
t.index ["user_id", "post_id"], name: "index_likes_on_user_id_and_post_id", unique: true
|
||||||
t.index ["user_id"], name: "index_likes_on_user_id"
|
t.index ["user_id"], name: "index_likes_on_user_id"
|
||||||
end
|
end
|
||||||
@@ -63,8 +71,10 @@ ActiveRecord::Schema.define(version: 2022_06_22_092039) do
|
|||||||
t.bigint "post_status_id"
|
t.bigint "post_status_id"
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.bigint "tenant_id", null: false
|
||||||
t.index ["post_id"], name: "index_post_status_changes_on_post_id"
|
t.index ["post_id"], name: "index_post_status_changes_on_post_id"
|
||||||
t.index ["post_status_id"], name: "index_post_status_changes_on_post_status_id"
|
t.index ["post_status_id"], name: "index_post_status_changes_on_post_status_id"
|
||||||
|
t.index ["tenant_id"], name: "index_post_status_changes_on_tenant_id"
|
||||||
t.index ["user_id"], name: "index_post_status_changes_on_user_id"
|
t.index ["user_id"], name: "index_post_status_changes_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -75,7 +85,9 @@ ActiveRecord::Schema.define(version: 2022_06_22_092039) do
|
|||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.integer "order", null: false
|
t.integer "order", null: false
|
||||||
t.boolean "show_in_roadmap", default: false, null: false
|
t.boolean "show_in_roadmap", default: false, null: false
|
||||||
t.index ["name"], name: "index_post_statuses_on_name", unique: true
|
t.bigint "tenant_id", null: false
|
||||||
|
t.index ["name", "tenant_id"], name: "index_post_statuses_on_name_and_tenant_id", unique: true
|
||||||
|
t.index ["tenant_id"], name: "index_post_statuses_on_tenant_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "posts", force: :cascade do |t|
|
create_table "posts", force: :cascade do |t|
|
||||||
@@ -86,11 +98,25 @@ ActiveRecord::Schema.define(version: 2022_06_22_092039) do
|
|||||||
t.bigint "post_status_id"
|
t.bigint "post_status_id"
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.bigint "tenant_id", null: false
|
||||||
t.index ["board_id"], name: "index_posts_on_board_id"
|
t.index ["board_id"], name: "index_posts_on_board_id"
|
||||||
t.index ["post_status_id"], name: "index_posts_on_post_status_id"
|
t.index ["post_status_id"], name: "index_posts_on_post_status_id"
|
||||||
|
t.index ["tenant_id"], name: "index_posts_on_tenant_id"
|
||||||
t.index ["user_id"], name: "index_posts_on_user_id"
|
t.index ["user_id"], name: "index_posts_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "tenants", force: :cascade do |t|
|
||||||
|
t.string "site_name", null: false
|
||||||
|
t.string "site_logo"
|
||||||
|
t.string "subdomain", null: false
|
||||||
|
t.string "locale", default: "en"
|
||||||
|
t.string "custom_url"
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.integer "brand_display_setting", default: 0
|
||||||
|
t.integer "status"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "users", force: :cascade do |t|
|
create_table "users", force: :cascade do |t|
|
||||||
t.string "email", default: "", null: false
|
t.string "email", default: "", null: false
|
||||||
t.string "encrypted_password", default: "", null: false
|
t.string "encrypted_password", default: "", null: false
|
||||||
@@ -107,22 +133,32 @@ ActiveRecord::Schema.define(version: 2022_06_22_092039) do
|
|||||||
t.string "full_name"
|
t.string "full_name"
|
||||||
t.boolean "notifications_enabled", default: true, null: false
|
t.boolean "notifications_enabled", default: true, null: false
|
||||||
t.integer "status"
|
t.integer "status"
|
||||||
|
t.bigint "tenant_id", null: false
|
||||||
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"], name: "index_users_on_email", 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
|
||||||
|
t.index ["tenant_id"], name: "index_users_on_tenant_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_foreign_key "boards", "tenants"
|
||||||
add_foreign_key "comments", "comments", column: "parent_id"
|
add_foreign_key "comments", "comments", column: "parent_id"
|
||||||
add_foreign_key "comments", "posts"
|
add_foreign_key "comments", "posts"
|
||||||
|
add_foreign_key "comments", "tenants"
|
||||||
add_foreign_key "comments", "users"
|
add_foreign_key "comments", "users"
|
||||||
add_foreign_key "follows", "posts"
|
add_foreign_key "follows", "posts"
|
||||||
|
add_foreign_key "follows", "tenants"
|
||||||
add_foreign_key "follows", "users"
|
add_foreign_key "follows", "users"
|
||||||
add_foreign_key "likes", "posts"
|
add_foreign_key "likes", "posts"
|
||||||
|
add_foreign_key "likes", "tenants"
|
||||||
add_foreign_key "likes", "users"
|
add_foreign_key "likes", "users"
|
||||||
add_foreign_key "post_status_changes", "post_statuses"
|
add_foreign_key "post_status_changes", "post_statuses"
|
||||||
add_foreign_key "post_status_changes", "posts"
|
add_foreign_key "post_status_changes", "posts"
|
||||||
|
add_foreign_key "post_status_changes", "tenants"
|
||||||
add_foreign_key "post_status_changes", "users"
|
add_foreign_key "post_status_changes", "users"
|
||||||
|
add_foreign_key "post_statuses", "tenants"
|
||||||
add_foreign_key "posts", "boards"
|
add_foreign_key "posts", "boards"
|
||||||
add_foreign_key "posts", "post_statuses"
|
add_foreign_key "posts", "post_statuses"
|
||||||
|
add_foreign_key "posts", "tenants"
|
||||||
add_foreign_key "posts", "users"
|
add_foreign_key "posts", "users"
|
||||||
|
add_foreign_key "users", "tenants"
|
||||||
end
|
end
|
||||||
|
|||||||
43
db/seeds.rb
43
db/seeds.rb
@@ -1,3 +1,11 @@
|
|||||||
|
# Create tenant
|
||||||
|
tenant = Tenant.create(
|
||||||
|
site_name: 'Default site name',
|
||||||
|
subdomain: 'default',
|
||||||
|
status: 'active'
|
||||||
|
)
|
||||||
|
Current.tenant = tenant
|
||||||
|
|
||||||
# Create an admin user and confirm its email automatically
|
# Create an admin user and confirm its email automatically
|
||||||
admin = User.create(
|
admin = User.create(
|
||||||
full_name: 'Admin',
|
full_name: 'Admin',
|
||||||
@@ -10,7 +18,7 @@ admin = User.create(
|
|||||||
# 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 they can be *Markdown formatted*.',
|
description: 'This is a **board**! You can create as many as you want from **site settings** and their description can be *Markdown formatted*.',
|
||||||
order: 0
|
order: 0
|
||||||
)
|
)
|
||||||
bug_board = Board.create(
|
bug_board = Board.create(
|
||||||
@@ -47,30 +55,33 @@ rejected_post_status = PostStatus.create(
|
|||||||
|
|
||||||
# Create some posts
|
# Create some posts
|
||||||
post1 = Post.create(
|
post1 = Post.create(
|
||||||
title: 'This is how users give you feedback',
|
title: 'Users can submit feedback by publishing posts!',
|
||||||
description: 'They can also provide an extendend description like this... bla bla...',
|
description: '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: admin.id
|
|
||||||
)
|
|
||||||
post2 = Post.create(
|
|
||||||
title: 'You can assign a status to each post',
|
|
||||||
description: 'This one, for example, is marked as "Planned"',
|
|
||||||
board_id: feature_board.id,
|
board_id: feature_board.id,
|
||||||
user_id: admin.id,
|
user_id: admin.id,
|
||||||
post_status_id: planned_post_status.id
|
post_status_id: planned_post_status.id
|
||||||
)
|
)
|
||||||
post3 = Post.create(
|
PostStatusChange.create(
|
||||||
title: 'There are multiple boards',
|
post_id: post1.id,
|
||||||
description: 'For now you have Feature Requests and Bug Reports, but you can add or remove as many as you want!',
|
|
||||||
board_id: bug_board.id,
|
|
||||||
user_id: admin.id,
|
user_id: admin.id,
|
||||||
post_status_id: in_progress_post_status.id
|
post_status_id: planned_post_status.id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create some comments
|
post2 = Post.create(
|
||||||
post1.comments.create(body: 'Users can comment to express their opinions!', user_id: admin.id)
|
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!',
|
||||||
|
board_id: bug_board.id,
|
||||||
|
user_id: admin.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# # Create some comments
|
||||||
|
post1.comments.create(
|
||||||
|
body: 'Users can comment to express their opinions! As with posts and board descriptions, comments can be *Markdown* **formatted**',
|
||||||
|
user_id: admin.id
|
||||||
|
)
|
||||||
|
|
||||||
# Let the user know how to log in with admin account
|
# Let the user know how to log in with admin account
|
||||||
|
puts "A default tenant has been created with name #{tenant.site_name}"
|
||||||
puts 'A default admin account has been created. Credentials:'
|
puts 'A default admin account has been created. Credentials:'
|
||||||
puts "-> email: #{admin.email}"
|
puts "-> email: #{admin.email}"
|
||||||
puts "-> password: #{admin.password}"
|
puts "-> password: #{admin.password}"
|
||||||
@@ -18,9 +18,7 @@ services:
|
|||||||
- POSTGRES_USER
|
- POSTGRES_USER
|
||||||
- POSTGRES_PASSWORD
|
- POSTGRES_PASSWORD
|
||||||
- EMAIL_CONFIRMATION
|
- EMAIL_CONFIRMATION
|
||||||
- APP_NAME
|
- MULTI_TENANCY
|
||||||
- SHOW_LOGO
|
|
||||||
- POSTS_PER_PAGE
|
|
||||||
volumes:
|
volumes:
|
||||||
- .:/astuto
|
- .:/astuto
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
9
spec/factories/tenants.rb
Normal file
9
spec/factories/tenants.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory :tenant do
|
||||||
|
site_name { "MySiteName" }
|
||||||
|
site_logo { "" }
|
||||||
|
sequence(:subdomain) { |n| "mysubdomain#{n}" }
|
||||||
|
locale { "en" }
|
||||||
|
custom_url { "" }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,8 +8,8 @@ RSpec.describe UserMailer, type: :mailer do
|
|||||||
let(:mail) { UserMailer.notify_post_owner(comment: comment) }
|
let(:mail) { UserMailer.notify_post_owner(comment: comment) }
|
||||||
|
|
||||||
it "renders the headers" do
|
it "renders the headers" do
|
||||||
expect(mail.to).to eq(["notified@example.com"])
|
expect(mail.to).to eq([user.email])
|
||||||
expect(mail.from).to eq(["notifications@example.com"])
|
expect(mail.from).to eq(["notifications@astuto.io"])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "renders the user name, post title, replier name and comment body" do
|
it "renders the user name, post title, replier name and comment body" do
|
||||||
|
|||||||
38
spec/models/tenant_spec.rb
Normal file
38
spec/models/tenant_spec.rb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Tenant, type: :model do
|
||||||
|
let(:tenant) { FactoryBot.build(:tenant) }
|
||||||
|
|
||||||
|
it 'should be valid' do
|
||||||
|
expect(tenant).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has status "pending" by default' do
|
||||||
|
expect(Tenant.new.status).to eq('pending')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a status of "active", "pending" or "blocked"' do
|
||||||
|
tenant.status = 'active'
|
||||||
|
expect(tenant).to be_valid
|
||||||
|
|
||||||
|
tenant.status = 'pending'
|
||||||
|
expect(tenant).to be_valid
|
||||||
|
|
||||||
|
tenant.status = 'blocked'
|
||||||
|
expect(tenant).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a non-empty site name' do
|
||||||
|
tenant.site_name = ''
|
||||||
|
expect(tenant).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a non-empty and unique subdomain' do
|
||||||
|
tenant.subdomain = ''
|
||||||
|
expect(tenant).to be_invalid
|
||||||
|
|
||||||
|
tenant2 = FactoryBot.create(:tenant)
|
||||||
|
tenant.subdomain = tenant2.subdomain
|
||||||
|
expect(tenant).to be_invalid
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'requests to posts controller', type: :request do
|
|
||||||
let(:user) { FactoryBot.create(:user) }
|
|
||||||
let(:moderator) { FactoryBot.create(:moderator) }
|
|
||||||
let(:admin) { FactoryBot.create(:admin) }
|
|
||||||
|
|
||||||
let(:p) { FactoryBot.create(:post) }
|
|
||||||
let(:board) { FactoryBot.build_stubbed(:board) }
|
|
||||||
|
|
||||||
let(:headers) { headers = { "ACCEPT" => "application/json" } }
|
|
||||||
|
|
||||||
context 'when user is not logged in' do
|
|
||||||
it 'fulfills index action' do
|
|
||||||
get posts_path
|
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
end
|
|
||||||
it 'fulfills show action' do
|
|
||||||
get posts_path(p)
|
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
end
|
|
||||||
it 'blocks create action' do
|
|
||||||
post posts_path, params: { post: { title: p.title } }, headers: headers
|
|
||||||
expect(response).to have_http_status(:unauthorized)
|
|
||||||
end
|
|
||||||
it 'blocks update action' do
|
|
||||||
patch post_path(p), params: { post: { title: p.title } }, headers: headers
|
|
||||||
expect(response).to have_http_status(:unauthorized)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'user role' do
|
|
||||||
before(:each) do
|
|
||||||
user.confirm
|
|
||||||
sign_in user
|
|
||||||
end
|
|
||||||
|
|
||||||
# it 'fulfills create action' do
|
|
||||||
# post posts_path, params: { post: { title: p.title, board_id: board.id, user_id: user.id } }, headers: headers
|
|
||||||
# expect(response).to have_http_status(:success)
|
|
||||||
# end
|
|
||||||
it 'blocks update action if from different user' do
|
|
||||||
expect(user.id).not_to eq(p.user_id)
|
|
||||||
patch post_path(p), params: { post: { title: p.title } }, headers: headers
|
|
||||||
expect(response).to have_http_status(:unauthorized)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'moderator role' do
|
|
||||||
before(:each) do
|
|
||||||
moderator.confirm
|
|
||||||
sign_in moderator
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fulfills update action' do
|
|
||||||
expect(user.id).not_to eq(p.user_id)
|
|
||||||
patch post_path(p), params: { post: { title: p.title } }, headers: headers
|
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
35
spec/routing/site_settings_routing_spec.rb
Normal file
35
spec/routing/site_settings_routing_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'site settings routing', :aggregate_failures, type: :routing do
|
||||||
|
let (:base_url) { '/site_settings' }
|
||||||
|
|
||||||
|
it 'routes general' do
|
||||||
|
expect(get: base_url + '/general').to route_to(
|
||||||
|
controller: 'site_settings', action: 'general'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes boards' do
|
||||||
|
expect(get: base_url + '/boards').to route_to(
|
||||||
|
controller: 'site_settings', action: 'boards'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes post statuses' do
|
||||||
|
expect(get: base_url + '/post_statuses').to route_to(
|
||||||
|
controller: 'site_settings', action: 'post_statuses'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes roadmap' do
|
||||||
|
expect(get: base_url + '/roadmap').to route_to(
|
||||||
|
controller: 'site_settings', action: 'roadmap'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes users' do
|
||||||
|
expect(get: base_url + '/users').to route_to(
|
||||||
|
controller: 'site_settings', action: 'users'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,4 +10,16 @@ RSpec.describe 'static pages routing', :aggregate_failures, type: :routing do
|
|||||||
controller: 'static_pages', action: 'roadmap'
|
controller: 'static_pages', action: 'roadmap'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'routes pending tenant page' do
|
||||||
|
expect(get: '/pending-tenant').to route_to(
|
||||||
|
controller: 'static_pages', action: 'pending_tenant'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes blocked tenant page' do
|
||||||
|
expect(get: '/blocked-tenant').to route_to(
|
||||||
|
controller: 'static_pages', action: 'blocked_tenant'
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -14,6 +14,16 @@
|
|||||||
#
|
#
|
||||||
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
|
# Reset Current instance and delete all tenants from test db
|
||||||
|
# Create a new default tenant and set Current.tenant
|
||||||
|
config.before(:all) do
|
||||||
|
Current.reset
|
||||||
|
Tenant.delete_all
|
||||||
|
|
||||||
|
tenant = FactoryBot.create(:tenant)
|
||||||
|
Current.tenant = tenant
|
||||||
|
end
|
||||||
|
|
||||||
# rspec-expectations config goes here. You can use an alternate
|
# rspec-expectations config goes here. You can use an alternate
|
||||||
# assertion/expectation library such as wrong or the stdlib/minitest
|
# assertion/expectation library such as wrong or the stdlib/minitest
|
||||||
# assertions if you prefer.
|
# assertions if you prefer.
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe 'header', type: :view do
|
|
||||||
let(:board1) { FactoryBot.create(:board) }
|
|
||||||
let(:board2) { FactoryBot.create(:board) }
|
|
||||||
|
|
||||||
def render_header
|
|
||||||
render partial: 'layouts/header'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders a logo' do
|
|
||||||
render_header
|
|
||||||
|
|
||||||
expect(rendered).to have_selector('.brand')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'renders a link for each board' do
|
|
||||||
@boards = [board1, board2]
|
|
||||||
|
|
||||||
render_header
|
|
||||||
|
|
||||||
expect(rendered).to have_content(board1.name)
|
|
||||||
expect(rendered).to have_content(board2.name)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'applies "active" class to the active board link' do
|
|
||||||
@boards = [board1, board2]
|
|
||||||
@board = board1 # active board is board1
|
|
||||||
|
|
||||||
render_header
|
|
||||||
|
|
||||||
expect(rendered).to have_selector('.active', count: 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user