diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 312f71cd..0f5ab590 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -52,7 +52,13 @@ class DashboardController < ApplicationController
def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc)
- rel = rel.where(folder_id: current_account.default_template_folder.id) if params[:q].blank?
+
+ if params[:q].blank?
+ shared_template_ids =
+ TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
+
+ rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
+ end
Templates.search(rel, params[:q])
end
diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb
index 9c6d6026..fcc7b90b 100644
--- a/app/controllers/submissions_controller.rb
+++ b/app/controllers/submissions_controller.rb
@@ -75,6 +75,6 @@ class SubmissionsController < ApplicationController
end
def load_template
- @template = current_account.templates.find(params[:template_id])
+ @template = Template.accessible_by(current_ability).find(params[:template_id])
end
end
diff --git a/app/controllers/template_sharings_testing_controller.rb b/app/controllers/template_sharings_testing_controller.rb
new file mode 100644
index 00000000..c8f64696
--- /dev/null
+++ b/app/controllers/template_sharings_testing_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class TemplateSharingsTestingController < ApplicationController
+ load_and_authorize_resource :template, parent: true
+
+ before_action do
+ authorize!(:manage, TemplateSharing.new(template: @template))
+ end
+
+ def create
+ testing_account = Accounts.find_or_create_testing_user(true_user.account).account
+
+ if params[:value] == '1'
+ TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
+ else
+ TemplateSharing.find_by(template: @template, account: testing_account)&.destroy!
+ end
+
+ head :ok
+ end
+end
diff --git a/app/controllers/templates_code_modal_controller.rb b/app/controllers/templates_code_modal_controller.rb
new file mode 100644
index 00000000..bc2549ab
--- /dev/null
+++ b/app/controllers/templates_code_modal_controller.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class TemplatesCodeModalController < ApplicationController
+ load_and_authorize_resource :template
+
+ def show; end
+end
diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb
index 05cb2bac..0123b248 100644
--- a/app/controllers/templates_controller.rb
+++ b/app/controllers/templates_controller.rb
@@ -6,7 +6,7 @@ class TemplatesController < ApplicationController
before_action :load_base_template, only: %i[new create]
def show
- submissions = @template.submissions
+ submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q])
@@ -47,11 +47,12 @@ class TemplatesController < ApplicationController
name: params.dig(:template, :name),
folder_name: params[:folder_name])
else
- @template.account = current_account
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
end
+ @template.account = current_account
+
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
@@ -87,6 +88,6 @@ class TemplatesController < ApplicationController
def load_base_template
return if params[:base_template_id].blank?
- @base_template = current_account.templates.find_by(id: params[:base_template_id])
+ @base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
end
end
diff --git a/app/controllers/templates_preview_controller.rb b/app/controllers/templates_preview_controller.rb
new file mode 100644
index 00000000..1cac478a
--- /dev/null
+++ b/app/controllers/templates_preview_controller.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class TemplatesPreviewController < ApplicationController
+ load_and_authorize_resource :template
+
+ def show
+ ActiveRecord::Associations::Preloader.new(
+ records: [@template],
+ associations: [schema_documents: { preview_images_attachments: :blob }]
+ ).call
+
+ @template_data =
+ @template.as_json.merge(
+ documents: @template.schema_documents.as_json(
+ methods: [:metadata],
+ include: { preview_images: { methods: %i[url metadata filename] } }
+ )
+ ).to_json
+
+ render :show, layout: 'plain'
+ end
+end
diff --git a/app/controllers/testing_accounts_controller.rb b/app/controllers/testing_accounts_controller.rb
index 1bce910e..6c6cf3d1 100644
--- a/app/controllers/testing_accounts_controller.rb
+++ b/app/controllers/testing_accounts_controller.rb
@@ -7,7 +7,7 @@ class TestingAccountsController < ApplicationController
authorize!(:manage, current_account)
authorize!(:manage, current_user)
- impersonate_user(Accounts.find_or_create_testing_user(current_account))
+ impersonate_user(Accounts.find_or_create_testing_user(true_user.account))
redirect_back(fallback_location: root_path)
end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index c5ae487d..dc2df889 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -88,6 +88,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5',
withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false',
+ editable: this.dataset.editable !== 'false',
withPayment: this.dataset.withPayment === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue
index 532f53a4..b0185da4 100644
--- a/app/javascript/template_builder/builder.vue
+++ b/app/javascript/template_builder/builder.vue
@@ -62,6 +62,7 @@
+
+
+ {{ t('back') }}
+
+
diff --git a/app/models/template.rb b/app/models/template.rb
index 7b3d7f1b..2e5be5a1 100644
--- a/app/models/template.rb
+++ b/app/models/template.rb
@@ -57,6 +57,7 @@ class Template < ApplicationRecord
class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record
has_many :submissions, dependent: :destroy
+ has_many :template_sharings, dependent: :destroy
scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
diff --git a/app/models/template_sharing.rb b/app/models/template_sharing.rb
new file mode 100644
index 00000000..b520fd2c
--- /dev/null
+++ b/app/models/template_sharing.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: template_sharings
+#
+# id :bigint not null, primary key
+# ability :string not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :bigint not null
+# template_id :bigint not null
+#
+# Indexes
+#
+# index_template_sharings_on_account_id_and_template_id (account_id,template_id) UNIQUE
+# index_template_sharings_on_template_id (template_id)
+#
+# Foreign Keys
+#
+# fk_rails_... (template_id => templates.id)
+#
+class TemplateSharing < ApplicationRecord
+ ALL_ID = -1
+
+ belongs_to :template
+ belongs_to :account, optional: true
+end
diff --git a/app/views/icons/_code.html.erb b/app/views/icons/_code.html.erb
new file mode 100644
index 00000000..c56b13ff
--- /dev/null
+++ b/app/views/icons/_code.html.erb
@@ -0,0 +1,6 @@
+
diff --git a/app/views/submissions/_link_form.html.erb b/app/views/submissions/_link_form.html.erb
index 01221296..47c2db7b 100644
--- a/app/views/submissions/_link_form.html.erb
+++ b/app/views/submissions/_link_form.html.erb
@@ -6,181 +6,7 @@
Or embed on your website
-
-
-
-
-
-
- <%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
-
-
-
-
-
-
<script src="<%= Docuseal::CDN_URL %>/js/form.js"></script>
-
-<docuseal-form data-src="<%= start_form_url(slug: template.slug) %>"></docuseal-form>
-
-
-
-
-
-
-
- <%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
-
-
-
-
-
-
import React from "react"
-import { DocusealForm } from '@docuseal/react'
-
-export function App() {
- return (
- <DocusealForm
- src="<%= start_form_url(slug: template.slug) %>"
- />
- );
-}
-
-
-
-
-
-
-
- <%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
-
-
-
-
-
-
<template>
- <DocusealForm
- :src="'<%= start_form_url(slug: template.slug) %>'"
- />
-</template>
-
-<script>
-import { DocusealForm } from '@docuseal/vue'
-
-export default {
- name: 'App',
- components: {
- DocusealForm
- }
-}
-</script>
-
-
-
+ <%= render 'templates/embedding', template: %>
diff --git a/app/views/templates/_embedding.html.erb b/app/views/templates/_embedding.html.erb
new file mode 100644
index 00000000..af6df88c
--- /dev/null
+++ b/app/views/templates/_embedding.html.erb
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+ <%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
+
+
+
+
+
+
<script src="<%= Docuseal::CDN_URL %>/js/form.js"></script>
+
+<docuseal-form data-src="<%= start_form_url(slug: template.slug) %>"></docuseal-form>
+
+
+
+
+
+
+
+ <%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
+
+
+
+
+
+
import React from "react"
+import { DocusealForm } from '@docuseal/react'
+
+export function App() {
+ return (
+ <DocusealForm
+ src="<%= start_form_url(slug: template.slug) %>"
+ />
+ );
+}
+
+
+
+
+
+
+
+ <%= link_to 'Learn More', console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), target: '_blank', data: { turbo: false }, class: 'btn btn-ghost text-gray-100 flex', rel: 'noopener' %>
+
+
+
+
+
+
<template>
+ <DocusealForm
+ :src="'<%= start_form_url(slug: template.slug) %>'"
+ />
+</template>
+
+<script>
+import { DocusealForm } from '@docuseal/vue'
+
+export default {
+ name: 'App',
+ components: {
+ DocusealForm
+ }
+}
+</script>
+
+
+
diff --git a/app/views/templates/_template.html.erb b/app/views/templates/_template.html.erb
index 67905060..18543244 100644
--- a/app/views/templates/_template.html.erb
+++ b/app/views/templates/_template.html.erb
@@ -7,6 +7,9 @@
<%= svg_icon('user', class: 'w-4 h-4') %>
<%= template.author.full_name.presence || template.author.email.to_s.sub(/\+\w+@/, '@') %>
+ <% if template.account_id != current_account.id %>
+ shared
+ <% end %>
@@ -24,7 +27,7 @@
@@ -41,12 +62,21 @@
<% end %>
<% end %>
- <% if !template.archived_at? && can?(:update, template) %>
- <%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
-
- <%= svg_icon('pencil', class: 'w-6 h-6') %>
- Edit
-
+ <% if !template.archived_at? %>
+ <% if can?(:update, template) %>
+ <%= link_to edit_template_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
+
+ <%= svg_icon('pencil', class: 'w-6 h-6') %>
+ Edit
+
+ <% end %>
+ <% elsif can?(:read, template) %>
+ <%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
+
+ <%= svg_icon('file_text', class: 'w-6 h-6') %>
+ Preview
+
+ <% end %>
<% end %>
<% end %>
<% if template.archived_at? && can?(:create, template) %>
diff --git a/app/views/templates_code_modal/_placeholder.html.erb b/app/views/templates_code_modal/_placeholder.html.erb
new file mode 100644
index 00000000..f63d207c
--- /dev/null
+++ b/app/views/templates_code_modal/_placeholder.html.erb
@@ -0,0 +1,11 @@
+
+ <%= svg_icon('info_circle', class: 'w-6 h-6') %>
+
+
API and Embedding
+
+ Unlock with DocuSeal Pro
+
+ ">Learn More
+
+
+
diff --git a/app/views/templates_code_modal/_preferences.html.erb b/app/views/templates_code_modal/_preferences.html.erb
new file mode 100644
index 00000000..e69de29b
diff --git a/app/views/templates_code_modal/show.html.erb b/app/views/templates_code_modal/show.html.erb
new file mode 100644
index 00000000..b20cf352
--- /dev/null
+++ b/app/views/templates_code_modal/show.html.erb
@@ -0,0 +1,36 @@
+<%= render 'shared/turbo_modal', title: 'API and Embedding', close_after_submit: false do %>
+
+
+
+
+ <%= render 'shared/clipboard_copy', icon: 'copy', text: @template.id, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
+
+
+
+
+
+
+ <%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
+
+
+ <%= render 'templates/embedding', template: @template %>
+ <% if can?(:manage, TemplateSharing.new(template: @template)) %>
+ <%= form_for '', url: template_sharings_testing_index_path, method: :post, html: { class: 'mt-1' } do |f| %>
+ <%= f.hidden_field :template_id, value: @template.id %>
+
+
+ Share template with Test Environment
+
+ <%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %>
+
+ <% end %>
+
+
+ <% end %>
+ <%= render 'templates_code_modal/preferences' %>
+ <%= render 'templates_code_modal/placeholder' %>
+<% end %>
diff --git a/app/views/templates_preview/show.html.erb b/app/views/templates_preview/show.html.erb
new file mode 100644
index 00000000..10be9fc5
--- /dev/null
+++ b/app/views/templates_preview/show.html.erb
@@ -0,0 +1 @@
+
diff --git a/config/routes.rb b/config/routes.rb
index 118335df..2d84c9be 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -70,11 +70,14 @@ Rails.application.routes.draw do
end
resources :templates_archived, only: %i[index], path: 'archived'
resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
+ resources :template_sharings_testing, only: %i[create]
resources :templates, only: %i[new create edit show destroy] do
resources :restore, only: %i[create], controller: 'templates_restore'
resources :archived, only: %i[index], controller: 'templates_archived_submissions'
resources :submissions, only: %i[new create]
resource :folder, only: %i[edit update], controller: 'templates_folders'
+ resource :preview, only: %i[show], controller: 'templates_preview'
+ resource :code_modal, only: %i[show], controller: 'templates_code_modal'
resources :submissions_export, only: %i[index new]
end
resources :preview_document_page, only: %i[show], path: '/preview/:attachment_uuid'
diff --git a/db/migrate/20240203113455_create_template_sharings.rb b/db/migrate/20240203113455_create_template_sharings.rb
new file mode 100644
index 00000000..b5e96c4f
--- /dev/null
+++ b/db/migrate/20240203113455_create_template_sharings.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateTemplateSharings < ActiveRecord::Migration[7.1]
+ def change
+ create_table :template_sharings do |t|
+ t.references :template, null: false, foreign_key: true, index: true
+ t.references :account, null: false, foreign_key: false, index: false
+ t.string :ability, null: false
+
+ t.index %i[account_id template_id], unique: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 735410e4..07f7930d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2024_02_03_113454) do
+ActiveRecord::Schema[7.1].define(version: 2024_02_03_113455) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -192,6 +192,16 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_03_113454) do
t.index ["author_id"], name: "index_template_folders_on_author_id"
end
+ create_table "template_sharings", force: :cascade do |t|
+ t.bigint "template_id", null: false
+ t.bigint "account_id", null: false
+ t.string "ability", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["account_id", "template_id"], name: "index_template_sharings_on_account_id_and_template_id", unique: true
+ t.index ["template_id"], name: "index_template_sharings_on_template_id"
+ end
+
create_table "templates", force: :cascade do |t|
t.string "slug", null: false
t.string "name", null: false
@@ -272,6 +282,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_03_113454) do
add_foreign_key "submitters", "submissions"
add_foreign_key "template_folders", "accounts"
add_foreign_key "template_folders", "users", column: "author_id"
+ add_foreign_key "template_sharings", "templates"
add_foreign_key "templates", "accounts"
add_foreign_key "templates", "template_folders", column: "folder_id"
add_foreign_key "templates", "users", column: "author_id"
diff --git a/lib/abilities/template_conditions.rb b/lib/abilities/template_conditions.rb
new file mode 100644
index 00000000..c1d8925b
--- /dev/null
+++ b/lib/abilities/template_conditions.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Abilities
+ module TemplateConditions
+ module_function
+
+ def collection(user, ability: nil)
+ shared_ids =
+ Template.joins(:template_sharings)
+ .where(template_sharings: { ability:,
+ account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact)
+ .select(:id)
+
+ Template.where(account_id: user.account_id).or(Template.where(id: shared_ids))
+ end
+
+ def entity(template, user:, ability: nil)
+ return true if template.account_id == user.account_id
+
+ account_ids = [user.account_id, TemplateSharing::ALL_ID]
+
+ template.template_sharings.any? do |e|
+ e.account_id.in?(account_ids) && (ability.nil? || e.ability == ability)
+ end
+ end
+ end
+end
diff --git a/lib/ability.rb b/lib/ability.rb
index abdcf2f9..b99b79ea 100644
--- a/lib/ability.rb
+++ b/lib/ability.rb
@@ -5,7 +5,14 @@ class Ability
def initialize(user)
can :manage, Template, account_id: user.account_id
+
+ can %i[read update create], Template,
+ Abilities::TemplateConditions.collection(user) do |template|
+ Abilities::TemplateConditions.entity(template, user:, ability: 'manage')
+ end
+
can :manage, TemplateFolder, account_id: user.account_id
+ can :manage, TemplateSharing, template: { account_id: user.account_id }
can :manage, Submission, account_id: user.account_id
can :manage, Submitter, submission: { account_id: user.account_id }
can :manage, User, account_id: user.account_id