mirror of https://github.com/docusealco/docuseal
parent
04ec2f8260
commit
10fd624bec
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesVersionsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def index
|
||||||
|
versions = @template.template_versions.order(id: :desc).preload(:author)
|
||||||
|
|
||||||
|
render json: versions.as_json(TemplateVersions::SERIALIZE_PARAMS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
version = @template.template_versions.find(params[:id])
|
||||||
|
|
||||||
|
render json: TemplateVersions.serialize(version)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal modal-open items-start !animate-none overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
/>
|
||||||
|
<div class="modal-box pt-4 pb-6 mt-20 w-full">
|
||||||
|
<div class="flex justify-between items-center border-b pb-2 mb-3 font-medium">
|
||||||
|
<span>{{ t('revisions') }}</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-xl"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
>×</a>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-1.5">
|
||||||
|
<li
|
||||||
|
v-for="revision in revisions"
|
||||||
|
:key="revision.id"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full text-left rounded-lg p-3 border border-dashed border-base-200 transition-colors disabled:cursor-default hover:bg-base-200 hover:border-base-200"
|
||||||
|
:disabled="loadingId !== null"
|
||||||
|
@click="viewRevision(revision.id)"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center gap-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>{{ formatDate(revision.created_at) }}</span>
|
||||||
|
<span class="-ml-0.5 flex items-center space-x-1 text-xs text-base-content/60 mt-0.5">
|
||||||
|
<IconUser class="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
|
<span class="truncate">{{ revision.author.full_name || revision.author.email }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="btn btn-sm btn-neutral text-white pointer-events-none flex-shrink-0">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="loadingId === revision.id"
|
||||||
|
class="w-4 h-4 animate-spin"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ t('view') }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="!revisions.length"
|
||||||
|
class="py-4 text-center text-base-content/60"
|
||||||
|
>
|
||||||
|
{{ t('no_revisions_yet') }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconUser, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RevisionsModal',
|
||||||
|
components: { IconUser, IconInnerShadowTop },
|
||||||
|
inject: ['t', 'baseFetch'],
|
||||||
|
props: {
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
revisions: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close', 'apply'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loadingId: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
viewRevision (id) {
|
||||||
|
if (this.loadingId !== null) return
|
||||||
|
|
||||||
|
this.loadingId = id
|
||||||
|
|
||||||
|
this.baseFetch(`/templates/${this.template.id}/versions/${id}`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((revision) => { this.$emit('apply', revision) })
|
||||||
|
.finally(() => { this.loadingId = null })
|
||||||
|
},
|
||||||
|
formatDate (string) {
|
||||||
|
return new Date(string).toLocaleString(this.locale || undefined, {
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: template_versions
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# data :text not null
|
||||||
|
# sha1 :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
# author_id :bigint not null
|
||||||
|
# template_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_template_versions_on_account_id (account_id)
|
||||||
|
# index_template_versions_on_author_id (author_id)
|
||||||
|
# index_template_versions_on_template_id_and_sha1 (template_id,sha1) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
|
# fk_rails_... (author_id => users.id)
|
||||||
|
# fk_rails_... (template_id => templates.id)
|
||||||
|
#
|
||||||
|
class TemplateVersion < ApplicationRecord
|
||||||
|
belongs_to :template
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :author, class_name: 'User'
|
||||||
|
|
||||||
|
attribute :data, :string, default: -> { {} }
|
||||||
|
|
||||||
|
serialize :data, coder: JSON
|
||||||
|
|
||||||
|
before_validation :set_account, on: :create
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
self.account ||= template.account
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateTemplateVersions < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
create_table :template_versions do |t|
|
||||||
|
t.references :template, null: false, foreign_key: true, index: false
|
||||||
|
t.references :account, null: false, foreign_key: true, index: true
|
||||||
|
t.references :author, null: false, foreign_key: { to_table: :users }, index: true
|
||||||
|
t.text :data, null: false
|
||||||
|
t.string :sha1, null: false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :template_versions, %i[template_id sha1], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module TemplateVersions
|
||||||
|
SERIALIZE_PARAMS = {
|
||||||
|
only: %i[id created_at],
|
||||||
|
include: { author: { only: %i[email], methods: %i[full_name] } }
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
DATA_FIELDS = %i[name schema submitters variables_schema fields].freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def find_or_create_for(template, author:)
|
||||||
|
data = build_data(template)
|
||||||
|
sha1 = Digest::SHA1.hexdigest(data.to_json)
|
||||||
|
|
||||||
|
version = template.template_versions.find_by(sha1:)
|
||||||
|
version ||= template.template_versions.create!(data:, sha1:, author:)
|
||||||
|
|
||||||
|
version
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize(version)
|
||||||
|
data = version.data.dup
|
||||||
|
|
||||||
|
data['documents'] = serialize_documents(version.template, data['schema'].to_a)
|
||||||
|
data['dynamic_documents'] = serialize_dynamic_documents(version.template, data['dynamic_documents'].to_a)
|
||||||
|
|
||||||
|
version.as_json(SERIALIZE_PARAMS).merge('data' => data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_data(template)
|
||||||
|
dynamic_uuids = template.schema.select { |e| e['dynamic'] }.pluck('attachment_uuid')
|
||||||
|
|
||||||
|
dynamic_documents =
|
||||||
|
if dynamic_uuids.present?
|
||||||
|
template.dynamic_documents.where(uuid: dynamic_uuids).as_json(only: %i[uuid body])
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
template.as_json(only: DATA_FIELDS).merge('dynamic_documents' => dynamic_documents)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize_documents(template, schema)
|
||||||
|
return [] if schema.blank?
|
||||||
|
|
||||||
|
template.documents_attachments
|
||||||
|
.where(uuid: schema.pluck('attachment_uuid'))
|
||||||
|
.preload(:blob, preview_images_attachments: :blob)
|
||||||
|
.as_json(
|
||||||
|
only: %i[id uuid],
|
||||||
|
methods: %i[metadata signed_key],
|
||||||
|
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize_dynamic_documents(template, dynamic_docs)
|
||||||
|
return [] if dynamic_docs.blank?
|
||||||
|
|
||||||
|
dynamic_docs_index = template.dynamic_documents
|
||||||
|
.where(uuid: dynamic_docs.pluck('uuid'))
|
||||||
|
.preload(attachments_attachments: :blob)
|
||||||
|
.index_by(&:uuid)
|
||||||
|
|
||||||
|
dynamic_docs.map do |attrs|
|
||||||
|
document = dynamic_docs_index[attrs['uuid']]
|
||||||
|
|
||||||
|
attachments_data = document.attachments_attachments.as_json(only: %i[uuid], methods: %i[url metadata filename])
|
||||||
|
|
||||||
|
attrs.merge('head' => document.head, 'attachments' => attachments_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in new issue