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