mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-13 18:24:58 +00:00
Compare commits
94 Commits
master-202
...
signature-
Author | SHA1 | Date | |
---|---|---|---|
fd69568842
|
|||
71aaf01687 | |||
a256307b82
|
|||
a6480191e5 | |||
19eb6f7ebb
|
|||
261bc88b5e
|
|||
4f18b1d2b2
|
|||
968835a262
|
|||
85dc9bdb2f
|
|||
c877076429
|
|||
418794e586 | |||
fd66dbf26e
|
|||
fde74b190d
|
|||
527cf23d4f
|
|||
1d708a481d
|
|||
ff5640e193
|
|||
d45de5405b
|
|||
7b322d7bab
|
|||
daef18408a
|
|||
91a4b45607
|
|||
29fa086fde
|
|||
508c4cd674
|
|||
9fe20b5e81
|
|||
d8ded80582
|
|||
d283d62049
|
|||
6cd336922f
|
|||
13dbbb6741
|
|||
1313b6f138
|
|||
3d53e7da65
|
|||
8589bada3f
|
|||
292034d64d
|
|||
3f7c5d23dc
|
|||
78445f0d65
|
|||
c329a1f1f8
|
|||
9d722110a6
|
|||
82e2b9a0f6
|
|||
40b8fae8ba
|
|||
b99ea3b17a
|
|||
3f80d62ca2
|
|||
118ae291e2
|
|||
5c0f3cb317
|
|||
a0b5c208eb
|
|||
7913a377c8
|
|||
7cd638c5fc
|
|||
071c5e3c55
|
|||
da6589ba87
|
|||
a563ba644e
|
|||
2213f6f429
|
|||
9a9d14eb5a
|
|||
4cc001a070
|
|||
6c52ff84a8 | |||
818d800384
|
|||
cef641ee24
|
|||
c4c7280b52
|
|||
d8ad8c3605
|
|||
803332ba5f
|
|||
479651b31e
|
|||
7bedf1b5b8
|
|||
6b764114e4
|
|||
03a150aa16
|
|||
81706a61ef
|
|||
debca1f474
|
|||
2e71808be1
|
|||
0c1d9ee4be
|
|||
87599425df
|
|||
86ec6f82da
|
|||
17f4c85fa5
|
|||
82cd77678b
|
|||
9e69c97250
|
|||
b4fa478177
|
|||
42438d5bb5 | |||
5287824dbe
|
|||
cfce531754
|
|||
83121c2a83
|
|||
5a467ae38d
|
|||
75005b4ed6 | |||
90c5b0341a
|
|||
5a5d259d18
|
|||
758a14366e
|
|||
e91fce524e
|
|||
7b06c80c2a | |||
cf2fe1bba7
|
|||
27df3b2c9b
|
|||
b350c0cfe8 | |||
611a968162
|
|||
ce80207d98
|
|||
5fc5369db6
|
|||
20e8b03588
|
|||
4b65ec9b54
|
|||
a8c5d1f660
|
|||
5f67a7aadc
|
|||
77d06d756a
|
|||
c4c5c860f0 | |||
d9b36533a2
|
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\DocStoreBundle\Workflow\AccompanyingCourseDocumentDuplicator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class DocumentAccompanyingCourseDuplicateController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private AccompanyingCourseDocumentDuplicator $documentWorkflowDuplicator,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/doc-store/accompanying-course-document/{id}/duplicate', name: 'chill_doc_store_accompanying_course_document_duplicate')]
|
||||
public function __invoke(AccompanyingCourseDocument $document, Request $request, Session $session): Response
|
||||
{
|
||||
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::SEE, $document)) {
|
||||
throw new AccessDeniedHttpException('not allowed to see this document');
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(AccompanyingCourseDocumentVoter::CREATE, $document->getCourse())) {
|
||||
throw new AccessDeniedHttpException('not allowed to create this document');
|
||||
}
|
||||
|
||||
$duplicated = $this->documentWorkflowDuplicator->duplicate($document);
|
||||
$this->entityManager->persist($duplicated);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('accompanying_course_document_edit', ['id' => $duplicated->getId(), 'course' => $duplicated->getCourse()->getId()])
|
||||
);
|
||||
}
|
||||
}
|
@@ -18,6 +18,8 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
@@ -257,11 +259,33 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Selectable<int, StoredObjectVersion>&Collection<int, StoredObjectVersion>
|
||||
*/
|
||||
public function getVersions(): Collection&Selectable
|
||||
{
|
||||
return $this->versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves versions sorted by a given order.
|
||||
*
|
||||
* @param 'ASC'|'DESC' $order the sorting order, default is Order::Ascending
|
||||
*
|
||||
* @return readableCollection&Selectable The ordered collection of versions
|
||||
*/
|
||||
public function getVersionsOrdered(string $order = 'ASC'): ReadableCollection&Selectable
|
||||
{
|
||||
$versions = $this->getVersions()->toArray();
|
||||
|
||||
match ($order) {
|
||||
'ASC' => usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $a->getVersion() <=> $b->getVersion()),
|
||||
'DESC' => usort($versions, static fn (StoredObjectVersion $a, StoredObjectVersion $b) => $b->getVersion() <=> $a->getVersion()),
|
||||
};
|
||||
|
||||
return new ArrayCollection($versions);
|
||||
}
|
||||
|
||||
public function hasCurrentVersion(): bool
|
||||
{
|
||||
return null !== $this->getCurrentVersion();
|
||||
@@ -272,6 +296,47 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return null !== $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is a version kept before conversion.
|
||||
*
|
||||
* @return bool true if a version is kept before conversion, false otherwise
|
||||
*/
|
||||
public function hasKeptBeforeConversionVersion(): bool
|
||||
{
|
||||
foreach ($this->getVersions() as $version) {
|
||||
foreach ($version->getPointInTimes() as $pointInTime) {
|
||||
if (StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION === $pointInTime->getReason()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the last version of the stored object that was kept before conversion.
|
||||
*
|
||||
* This method iterates through the ordered versions and their respective points
|
||||
* in time to find the most recent version that has a point in time with the reason
|
||||
* 'KEEP_BEFORE_CONVERSION'.
|
||||
*
|
||||
* @return StoredObjectVersion|null the version that was kept before conversion,
|
||||
* or null if not found
|
||||
*/
|
||||
public function getLastKeptBeforeConversionVersion(): ?StoredObjectVersion
|
||||
{
|
||||
foreach ($this->getVersionsOrdered('DESC') as $version) {
|
||||
foreach ($version->getPointInTimes() as $pointInTime) {
|
||||
if (StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION === $pointInTime->getReason()) {
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setTemplate(?DocGeneratorTemplate $template): StoredObject
|
||||
{
|
||||
$this->template = $template;
|
||||
|
@@ -23,7 +23,7 @@ class AccompanyingCourseDocumentRepository implements ObjectRepository, Associat
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $em)
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->repository = $em->getRepository(AccompanyingCourseDocument::class);
|
||||
}
|
||||
|
@@ -0,0 +1,27 @@
|
||||
import {_createI18n} from "../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
|
||||
import {createApp} from "vue";
|
||||
import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.vue";
|
||||
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
||||
import {defineComponent} from "vue";
|
||||
import DownloadButton from "../../vuejs/StoredObjectButton/DownloadButton.vue";
|
||||
import ToastPlugin from "vue-toast-notification";
|
||||
|
||||
|
||||
|
||||
const i18n = _createI18n({});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function (e) {
|
||||
document.querySelectorAll<HTMLDivElement>('div[data-download-button-single]').forEach((el) => {
|
||||
const storedObject = JSON.parse(el.dataset.storedObject as string) as StoredObject;
|
||||
const title = el.dataset.title as string;
|
||||
const app = createApp({
|
||||
components: {DownloadButton},
|
||||
data() {
|
||||
return {storedObject, title, classes: {btn: true, "btn-outline-primary": true}};
|
||||
},
|
||||
template: '<download-button :stored-object="storedObject" :at-version="storedObject.currentVersion" :classes="classes" :filename="title" :direct-download="true"></download-button>',
|
||||
});
|
||||
|
||||
app.use(i18n).use(ToastPlugin).mount(el);
|
||||
});
|
||||
});
|
@@ -2,6 +2,7 @@ import {
|
||||
DateTime,
|
||||
User,
|
||||
} from "../../../ChillMainBundle/Resources/public/types";
|
||||
import {SignedUrlGet} from "./vuejs/StoredObjectButton/helpers";
|
||||
|
||||
export type StoredObjectStatus = "empty" | "ready" | "failure" | "pending";
|
||||
|
||||
@@ -30,6 +31,7 @@ export interface StoredObject {
|
||||
href: string;
|
||||
expiration: number;
|
||||
};
|
||||
downloadLink?: SignedUrlGet;
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)" title="Télécharger">
|
||||
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open()" title="Télécharger">
|
||||
<i class="fa fa-download"></i>
|
||||
<template v-if="displayActionStringInButton">Télécharger</template>
|
||||
</a>
|
||||
@@ -20,7 +20,15 @@ interface DownloadButtonConfig {
|
||||
atVersion: StoredObjectVersion,
|
||||
classes: { [k: string]: boolean },
|
||||
filename?: string,
|
||||
displayActionStringInButton: boolean,
|
||||
/**
|
||||
* if true, display the action string into the button. If false, displays only
|
||||
* the icon
|
||||
*/
|
||||
displayActionStringInButton?: boolean,
|
||||
/**
|
||||
* if true, will download directly the file on load
|
||||
*/
|
||||
directDownload?: boolean,
|
||||
}
|
||||
|
||||
interface DownloadButtonState {
|
||||
@@ -29,13 +37,17 @@ interface DownloadButtonState {
|
||||
href_url: string,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true});
|
||||
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true, directDownload: false});
|
||||
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
|
||||
|
||||
const open_button = ref<HTMLAnchorElement | null>(null);
|
||||
|
||||
function buildDocumentName(): string {
|
||||
const document_name = props.filename ?? props.storedObject.title ?? 'document';
|
||||
let document_name = props.filename ?? props.storedObject.title;
|
||||
|
||||
if ('' === document_name) {
|
||||
document_name = 'document';
|
||||
}
|
||||
|
||||
const ext = mime.getExtension(props.atVersion.type);
|
||||
|
||||
@@ -46,9 +58,7 @@ function buildDocumentName(): string {
|
||||
return document_name;
|
||||
}
|
||||
|
||||
async function download_and_open(event: Event): Promise<void> {
|
||||
const button = event.target as HTMLAnchorElement;
|
||||
|
||||
async function download_and_open(): Promise<void> {
|
||||
if (state.is_running) {
|
||||
console.log('state is running, aborting');
|
||||
return;
|
||||
@@ -75,11 +85,13 @@ async function download_and_open(event: Event): Promise<void> {
|
||||
state.is_running = false;
|
||||
state.is_ready = true;
|
||||
|
||||
await nextTick();
|
||||
open_button.value?.click();
|
||||
console.log('open button should have been clicked');
|
||||
if (!props.directDownload) {
|
||||
await nextTick();
|
||||
open_button.value?.click();
|
||||
|
||||
const timer = setTimeout(reset_state, 45000);
|
||||
console.log('open button should have been clicked');
|
||||
setTimeout(reset_state, 45000);
|
||||
}
|
||||
}
|
||||
|
||||
function reset_state(): void {
|
||||
@@ -87,10 +99,19 @@ function reset_state(): void {
|
||||
state.is_ready = false;
|
||||
state.is_running = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.directDownload) {
|
||||
download_and_open();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
i.fa {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
@@ -50,7 +50,6 @@ const onRestored = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTim
|
||||
:version="v"
|
||||
:can-edit="canEdit"
|
||||
:is-current="higher_version === v.version"
|
||||
:is-restored="v.version === state.restored"
|
||||
:stored-object="storedObject"
|
||||
@restore-version="onRestored"
|
||||
></history-button-list-item>
|
||||
|
@@ -12,7 +12,6 @@ interface HistoryButtonListItemConfig {
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
isCurrent: boolean;
|
||||
isRestored: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -31,7 +30,9 @@ const isKeptBeforeConversion = computed<boolean>(() => props.version["point-in-t
|
||||
),
|
||||
);
|
||||
|
||||
const isRestored = computed<boolean>(() => null !== props.version["from-restored"]);
|
||||
const isRestored = computed<boolean>(() => props.version.version > 0 && null !== props.version["from-restored"]);
|
||||
|
||||
const isDuplicated = computed<boolean>(() => props.version.version === 0 && null !== props.version["from-restored"]);
|
||||
|
||||
const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, 'blinking-2': boolean}>(() => ({row: true, 'row-hover': true, 'blinking-1': props.isRestored && 0 === props.version.version % 2, 'blinking-2': props.isRestored && 1 === props.version.version % 2}));
|
||||
|
||||
@@ -39,13 +40,14 @@ const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, '
|
||||
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored">
|
||||
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored || isDuplicated">
|
||||
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
|
||||
<span class="badge bg-info" v-if="isKeptBeforeConversion">Conservée avant conversion dans un autre format</span>
|
||||
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version }}</span>
|
||||
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version + 1 }}</span>
|
||||
<span class="badge bg-info" v-if="isDuplicated">Dupliqué depuis un autre document</span>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}
|
||||
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <template v-if="version.createdBy !== null && version.createdAt !== null"><strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template><template v-if="version.createdBy === null && version.createdAt !== null"><strong v-if="version.version == 0">Créé le</strong><strong v-else>modifié le</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}</template>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<ul class="record_actions small slim on-version-actions">
|
||||
|
@@ -22,7 +22,6 @@ const props = defineProps<HistoryButtonListConfig>();
|
||||
const state = reactive<HistoryButtonModalState>({opened: false});
|
||||
|
||||
const open = () => {
|
||||
console.log('open');
|
||||
state.opened = true;
|
||||
}
|
||||
|
||||
|
@@ -161,7 +161,14 @@ async function download_and_decrypt_doc(storedObject: StoredObject, atVersion: n
|
||||
throw new Error("no version associated to stored object");
|
||||
}
|
||||
|
||||
const downloadInfo= await download_info_link(storedObject, atVersionToDownload);
|
||||
// sometimes, the downloadInfo may be embedded into the storedObject
|
||||
console.log('storedObject', storedObject);
|
||||
let downloadInfo;
|
||||
if (typeof storedObject._links !== 'undefined' && typeof storedObject._links.downloadLink !== 'undefined') {
|
||||
downloadInfo = storedObject._links.downloadLink;
|
||||
} else {
|
||||
downloadInfo = await download_info_link(storedObject, atVersionToDownload);
|
||||
}
|
||||
|
||||
const rawResponse = await window.fetch(downloadInfo.url);
|
||||
|
||||
|
@@ -0,0 +1 @@
|
||||
<div data-download-button-single="data-download-button-single" data-stored-object="{{ document_json|json_encode|escape('html_attr') }}" data-title="{{ title|escape('html_attr') }}"></div>
|
@@ -73,8 +73,15 @@
|
||||
<li>
|
||||
{{ document.object|chill_document_button_group(document.title) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
|
||||
<li class="delete">
|
||||
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_CREATE', document.course) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
<a href="{{ chill_path_add_return_path('chill_doc_store_accompanying_course_document_duplicate', {'id': document.id}) }}" class="btn btn-duplicate" title="{{ 'Duplicate'|trans|e('html_attr') }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
|
||||
@@ -82,9 +89,9 @@
|
||||
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
|
||||
<li class="delete">
|
||||
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
@@ -0,0 +1,43 @@
|
||||
{% extends '@ChillMain/Workflow/workflow_view_send_public_layout.html.twig' %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_document_download_button') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_document_download_button') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{{ 'workflow.public_link.title'|trans }} - {{ title }}{% endblock %}
|
||||
|
||||
{% block public_content %}
|
||||
<h1>{{ 'workflow.public_link.shared_doc'|trans }}</h1>
|
||||
|
||||
{% set previous = send.entityWorkflowStepChained.previous %}
|
||||
{% if previous is not null %}
|
||||
{% if previous.transitionBy is not null %}
|
||||
<p>{{ 'workflow.public_link.doc_shared_by_at_explanation'|trans({'byUser': previous.transitionBy|chill_entity_render_string( { 'at_date': previous.transitionAt } ), 'at': previous.transitionAt }) }}</p>
|
||||
{% else %}
|
||||
<p>{{ 'workflow.public_link.doc_shared_automatically_at_explanation'|trans({'at': previous.transitionAt}) }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-6 col-md-4">
|
||||
<div class="card"">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ title }}</h2>
|
||||
<h3>{{ 'workflow.public_link.main_document'|trans }}</h3>
|
||||
|
||||
<ul class="record_actions slim small">
|
||||
<li>
|
||||
{{ storedObject|chill_document_download_only_button(storedObject.title(), false) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||
@@ -30,10 +31,17 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
/**
|
||||
* when added to the groups, a download link is included in the normalization,
|
||||
* and no webdav links are generated.
|
||||
*/
|
||||
public const DOWNLOAD_LINK_ONLY = 'read:download-link-only';
|
||||
|
||||
public function __construct(
|
||||
private readonly JWTDavTokenProviderInterface $JWTDavTokenProvider,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly Security $security,
|
||||
private readonly TempUrlGeneratorInterface $tempUrlGenerator,
|
||||
) {}
|
||||
|
||||
public function normalize($object, ?string $format = null, array $context = [])
|
||||
@@ -55,6 +63,24 @@ final class StoredObjectNormalizer implements NormalizerInterface, NormalizerAwa
|
||||
// deprecated property
|
||||
$datas['creationDate'] = $datas['createdAt'];
|
||||
|
||||
if (array_key_exists(AbstractNormalizer::GROUPS, $context)) {
|
||||
$groupsNormalized = is_array($context[AbstractNormalizer::GROUPS]) ? $context[AbstractNormalizer::GROUPS] : [$context[AbstractNormalizer::GROUPS]];
|
||||
} else {
|
||||
$groupsNormalized = [];
|
||||
}
|
||||
|
||||
if (in_array(self::DOWNLOAD_LINK_ONLY, $groupsNormalized, true)) {
|
||||
$datas['_permissions'] = [
|
||||
'canSee' => true,
|
||||
'canEdit' => false,
|
||||
];
|
||||
$datas['_links'] = [
|
||||
'downloadLink' => $this->normalizer->normalize($this->tempUrlGenerator->generate('GET', $object->getCurrentVersion()->getFilename(), 180), $format, [AbstractNormalizer::GROUPS => ['read']]),
|
||||
];
|
||||
|
||||
return $datas;
|
||||
}
|
||||
|
||||
$canSee = $this->security->isGranted(StoredObjectRoleEnum::SEE->value, $object);
|
||||
$canEdit = $this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $object);
|
||||
|
||||
|
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Class which duplicate a stored object into a new one, recreating a stored object.
|
||||
*/
|
||||
class StoredObjectDuplicate
|
||||
{
|
||||
public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {}
|
||||
|
||||
public function duplicate(StoredObject|StoredObjectVersion $from, bool $onlyLastKeptBeforeConversionVersion = true): StoredObject
|
||||
{
|
||||
$storedObject = $from instanceof StoredObjectVersion ? $from->getStoredObject() : $from;
|
||||
|
||||
$fromVersion = match ($storedObject->hasKeptBeforeConversionVersion() && $onlyLastKeptBeforeConversionVersion) {
|
||||
true => $from->getLastKeptBeforeConversionVersion(),
|
||||
false => $storedObject->getCurrentVersion(),
|
||||
};
|
||||
|
||||
if (null === $fromVersion) {
|
||||
throw new \UnexpectedValueException('could not find a version to restore');
|
||||
}
|
||||
|
||||
$oldContent = $this->storedObjectManager->read($fromVersion);
|
||||
|
||||
$storedObject = new StoredObject();
|
||||
|
||||
$newVersion = $this->storedObjectManager->write($storedObject, $oldContent, $fromVersion->getType());
|
||||
|
||||
$newVersion->setCreatedFrom($fromVersion);
|
||||
|
||||
$this->logger->info('[StoredObjectDuplicate] Duplicated stored object from a version of a previous stored object', [
|
||||
'from_stored_object_uuid' => $fromVersion->getStoredObject()->getUuid(),
|
||||
'to_stored_object_uuid' => $storedObject->getUuid(),
|
||||
'old_version_id' => $fromVersion->getId(),
|
||||
'old_version_version' => $fromVersion->getVersion(),
|
||||
'new_version_id' => $newVersion->getVersion(),
|
||||
]);
|
||||
|
||||
return $storedObject;
|
||||
}
|
||||
}
|
@@ -14,6 +14,9 @@ namespace Chill\DocStoreBundle\Service;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Class responsible for restoring stored object versions into the same stored object.
|
||||
*/
|
||||
final readonly class StoredObjectRestore implements StoredObjectRestoreInterface
|
||||
{
|
||||
public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {}
|
||||
|
@@ -11,25 +11,49 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class WorkflowStoredObjectPermissionHelper
|
||||
{
|
||||
public function __construct(private readonly Security $security, private readonly EntityWorkflowManager $entityWorkflowManager) {}
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
private readonly Registry $registry,
|
||||
) {}
|
||||
|
||||
public function notBlockedByWorkflow(object $entity): bool
|
||||
{
|
||||
$workflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
foreach ($workflows as $workflow) {
|
||||
if ($workflow->isFinal()) {
|
||||
return false;
|
||||
foreach ($entityWorkflows as $entityWorkflow) {
|
||||
if ($entityWorkflow->isFinal()) {
|
||||
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||
foreach ($marking->getPlaces() as $place => $active) {
|
||||
$metadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||
if ($metadata['isFinalPositive'] ?? true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$workflow->getCurrentStep()->getAllDestUser()->contains($currentUser)) {
|
||||
return false;
|
||||
// as soon as there is one signatured applyied, we are not able to
|
||||
// edit the document any more
|
||||
foreach ($entityWorkflow->getSteps() as $step) {
|
||||
foreach ($step->getSignatures() as $signature) {
|
||||
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -28,6 +28,10 @@ class WopiEditTwigExtension extends AbstractExtension
|
||||
'needs_environment' => true,
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
new TwigFilter('chill_document_download_only_button', [WopiEditTwigExtensionRuntime::class, 'renderDownloadButton'], [
|
||||
'needs_environment' => true,
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,7 @@ use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
@@ -177,6 +178,17 @@ final readonly class WopiEditTwigExtensionRuntime implements RuntimeExtensionInt
|
||||
]);
|
||||
}
|
||||
|
||||
public function renderDownloadButton(Environment $environment, StoredObject $storedObject, string $title = ''): string
|
||||
{
|
||||
return $environment->render(
|
||||
'@ChillDocStore/Button/button_download.html.twig',
|
||||
[
|
||||
'document_json' => $this->normalizer->normalize($storedObject, 'json', [AbstractNormalizer::GROUPS => ['read', StoredObjectNormalizer::DOWNLOAD_LINK_ONLY]]),
|
||||
'title' => $title,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string
|
||||
{
|
||||
return $environment->render(self::TEMPLATE, [
|
||||
|
@@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Tests\Entity;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
@@ -54,4 +56,27 @@ class StoredObjectTest extends KernelTestCase
|
||||
|
||||
self::assertNotSame($firstVersion, $version);
|
||||
}
|
||||
|
||||
public function testHasKeptBeforeConversionVersion(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$version1 = $storedObject->registerVersion();
|
||||
|
||||
self::assertFalse($storedObject->hasKeptBeforeConversionVersion());
|
||||
|
||||
// add a point in time without the correct version
|
||||
new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BY_USER);
|
||||
|
||||
self::assertFalse($storedObject->hasKeptBeforeConversionVersion());
|
||||
self::assertNull($storedObject->getLastKeptBeforeConversionVersion());
|
||||
|
||||
new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
|
||||
|
||||
self::assertTrue($storedObject->hasKeptBeforeConversionVersion());
|
||||
// add a second version
|
||||
$version2 = $storedObject->registerVersion();
|
||||
new StoredObjectPointInTime($version2, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
|
||||
|
||||
self::assertSame($version2, $storedObject->getLastKeptBeforeConversionVersion());
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Form;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Form\DataMapper\StoredObjectDataMapper;
|
||||
use Chill\DocStoreBundle\Form\DataTransformer\StoredObjectDataTransformer;
|
||||
@@ -132,7 +133,8 @@ class StoredObjectTypeTest extends TypeTestCase
|
||||
new StoredObjectNormalizer(
|
||||
$jwtTokenProvider->reveal(),
|
||||
$urlGenerator->reveal(),
|
||||
$security->reveal()
|
||||
$security->reveal(),
|
||||
$this->createMock(TempUrlGeneratorInterface::class)
|
||||
),
|
||||
new StoredObjectDenormalizer($storedObjectRepository->reveal()),
|
||||
new StoredObjectVersionNormalizer(),
|
||||
|
@@ -11,6 +11,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\AsyncUpload\SignedUrl;
|
||||
use Chill\DocStoreBundle\AsyncUpload\TempUrlGeneratorInterface;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Guard\JWTDavTokenProviderInterface;
|
||||
@@ -70,7 +72,9 @@ class StoredObjectNormalizerTest extends TestCase
|
||||
return ['sub' => 'sub'];
|
||||
});
|
||||
|
||||
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security);
|
||||
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||
|
||||
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security, $tempUrlGenerator);
|
||||
$normalizer->setNormalizer($globalNormalizer);
|
||||
|
||||
$actual = $normalizer->normalize($storedObject, 'json');
|
||||
@@ -95,4 +99,48 @@ class StoredObjectNormalizerTest extends TestCase
|
||||
self::assertArrayHasKey('dav_link', $actual['_links']);
|
||||
self::assertEqualsCanonicalizing(['href' => $davLink, 'expiration' => $d->getTimestamp()], $actual['_links']['dav_link']);
|
||||
}
|
||||
|
||||
public function testWithDownloadLinkOnly(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$storedObject->registerVersion();
|
||||
$storedObject->setTitle('test');
|
||||
$reflection = new \ReflectionClass(StoredObject::class);
|
||||
$idProperty = $reflection->getProperty('id');
|
||||
$idProperty->setValue($storedObject, 1);
|
||||
|
||||
$jwtProvider = $this->createMock(JWTDavTokenProviderInterface::class);
|
||||
$jwtProvider->expects($this->never())->method('createToken')->withAnyParameters();
|
||||
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator->expects($this->never())->method('generate');
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->expects($this->never())->method('isGranted');
|
||||
|
||||
$globalNormalizer = $this->createMock(NormalizerInterface::class);
|
||||
$globalNormalizer->expects($this->exactly(4))->method('normalize')
|
||||
->withAnyParameters()
|
||||
->willReturnCallback(function (?object $object, string $format, array $context) {
|
||||
if (null === $object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ['sub' => 'sub'];
|
||||
});
|
||||
|
||||
$tempUrlGenerator = $this->createMock(TempUrlGeneratorInterface::class);
|
||||
$tempUrlGenerator->expects($this->once())->method('generate')->with('GET', $storedObject->getCurrentVersion()->getFilename(), $this->isType('int'))
|
||||
->willReturn(new SignedUrl('GET', 'https://some-link/test', new \DateTimeImmutable('300 seconds'), $storedObject->getCurrentVersion()->getFilename()));
|
||||
|
||||
$normalizer = new StoredObjectNormalizer($jwtProvider, $urlGenerator, $security, $tempUrlGenerator);
|
||||
$normalizer->setNormalizer($globalNormalizer);
|
||||
|
||||
$actual = $normalizer->normalize($storedObject, 'json', ['groups' => ['read', 'read:download-link-only']]);
|
||||
|
||||
self::assertIsArray($actual);
|
||||
self::assertArrayHasKey('_links', $actual);
|
||||
self::assertArrayHasKey('downloadLink', $actual['_links']);
|
||||
self::assertEquals(['sub' => 'sub'], $actual['_links']['downloadLink']);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectDuplicateTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testDuplicateHappyScenario(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
// we create multiple version, we want the last to be duplicated
|
||||
$storedObject->registerVersion(type: 'application/test');
|
||||
$version = $storedObject->registerVersion(type: $type = 'application/test');
|
||||
|
||||
$manager = $this->createMock(StoredObjectManagerInterface::class);
|
||||
$manager->method('read')->with($version)->willReturn('1234');
|
||||
$manager
|
||||
->expects($this->once())
|
||||
->method('write')
|
||||
->with($this->isInstanceOf(StoredObject::class), '1234', 'application/test')
|
||||
->willReturnCallback(fn (StoredObject $so, $content, $type) => $so->registerVersion(type: $type));
|
||||
|
||||
$storedObjectDuplicate = new StoredObjectDuplicate($manager, new NullLogger());
|
||||
|
||||
$actual = $storedObjectDuplicate->duplicate($storedObject);
|
||||
|
||||
self::assertNotNull($actual->getCurrentVersion());
|
||||
self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom());
|
||||
self::assertSame($version, $actual->getCurrentVersion()->getCreatedFrom());
|
||||
}
|
||||
|
||||
public function testDuplicateWithKeptVersion(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
// we create two versions for stored object
|
||||
// the first one is "kept before conversion", and that one should
|
||||
// be duplicated, not the second one
|
||||
$version1 = $storedObject->registerVersion(type: $type = 'application/test');
|
||||
new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
|
||||
$version2 = $storedObject->registerVersion(type: $type = 'application/test');
|
||||
|
||||
$manager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
|
||||
// we create both possibilities for the method "read"
|
||||
$manager->read($version1)->willReturn('1234');
|
||||
$manager->read($version2)->willReturn('4567');
|
||||
|
||||
// we create the write method, and check that it is called with the content from version1, not version2
|
||||
$manager->write(Argument::type(StoredObject::class), '1234', 'application/test')
|
||||
->shouldBeCalled()
|
||||
->will(function ($args) {
|
||||
/** @var StoredObject $storedObject */
|
||||
$storedObject = $args[0]; // args are ordered by key, so the first one is the stored object...
|
||||
$type = $args[2]; // and the last one is the string $type
|
||||
|
||||
return $storedObject->registerVersion(type: $type);
|
||||
});
|
||||
|
||||
// we create the service which will duplicate things
|
||||
$storedObjectDuplicate = new StoredObjectDuplicate($manager->reveal(), new NullLogger());
|
||||
|
||||
$actual = $storedObjectDuplicate->duplicate($storedObject);
|
||||
|
||||
self::assertNotNull($actual->getCurrentVersion());
|
||||
self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom());
|
||||
self::assertSame($version1, $actual->getCurrentVersion()->getCreatedFrom());
|
||||
}
|
||||
|
||||
public function testDuplicateWithKeptVersionButWeWantToDuplicateTheLastOne(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
// we create two versions for stored object
|
||||
// the first one is "kept before conversion", and that one should
|
||||
// be duplicated, not the second one
|
||||
$version1 = $storedObject->registerVersion(type: $type = 'application/test');
|
||||
new StoredObjectPointInTime($version1, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION);
|
||||
$version2 = $storedObject->registerVersion(type: $type = 'application/test');
|
||||
|
||||
$manager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
|
||||
// we create both possibilities for the method "read"
|
||||
$manager->read($version1)->willReturn('1234');
|
||||
$manager->read($version2)->willReturn('4567');
|
||||
|
||||
// we create the write method, and check that it is called with the content from version1, not version2
|
||||
$manager->write(Argument::type(StoredObject::class), '4567', 'application/test')
|
||||
->shouldBeCalled()
|
||||
->will(function ($args) {
|
||||
/** @var StoredObject $storedObject */
|
||||
$storedObject = $args[0]; // args are ordered by key, so the first one is the stored object...
|
||||
$type = $args[2]; // and the last one is the string $type
|
||||
|
||||
return $storedObject->registerVersion(type: $type);
|
||||
});
|
||||
|
||||
// we create the service which will duplicate things
|
||||
$storedObjectDuplicate = new StoredObjectDuplicate($manager->reveal(), new NullLogger());
|
||||
|
||||
$actual = $storedObjectDuplicate->duplicate($storedObject, false);
|
||||
|
||||
self::assertNotNull($actual->getCurrentVersion());
|
||||
self::assertNotNull($actual->getCurrentVersion()->getCreatedFrom());
|
||||
self::assertSame($version2, $actual->getCurrentVersion()->getCreatedFrom());
|
||||
}
|
||||
}
|
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||
use Symfony\Component\Workflow\Workflow;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class WorkflowStoredObjectPermissionHelperTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @dataProvider provideDataNotBlockByWorkflow
|
||||
*/
|
||||
public function testNotBlockByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void
|
||||
{
|
||||
// all entities must have this workflow name, so we are ok to set it here
|
||||
$entityWorkflow->setWorkflowName('dummy');
|
||||
$object = new \stdClass();
|
||||
$helper = $this->buildHelper($object, $entityWorkflow, $user);
|
||||
|
||||
self::assertEquals($expected, $helper->notBlockedByWorkflow($entityWorkflow), $message);
|
||||
}
|
||||
|
||||
private function buildHelper(object $relatedEntity, EntityWorkflow $entityWorkflow, User $user): WorkflowStoredObjectPermissionHelper
|
||||
{
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn($user);
|
||||
|
||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]);
|
||||
|
||||
return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry());
|
||||
}
|
||||
|
||||
public static function provideDataNotBlockByWorkflow(): iterable
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
|
||||
|
||||
yield [$entityWorkflow, new User(), false, 'blocked because the user is not present as a dest user'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
||||
|
||||
yield [$entityWorkflow, $user, true, 'allowed because the user is present as a dest user'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), $user);
|
||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||
|
||||
yield [$entityWorkflow, $user, false, 'blocked because the step is final, and final positive'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||
|
||||
yield [$entityWorkflow, $user, true, 'allowed because the step is final, and final negative'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
new EntityWorkflowStepSignature($step, new Person());
|
||||
|
||||
yield [$entityWorkflow, $user, true, 'allow, a signature is present but still pending'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
$signature = new EntityWorkflowStepSignature($step, new Person());
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
||||
|
||||
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed'];
|
||||
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$dto->futureDestUsers[] = $user = new User();
|
||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
||||
$step = $entityWorkflow->getCurrentStep();
|
||||
$signature = new EntityWorkflowStepSignature($step, new Person());
|
||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
||||
|
||||
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed, although the workflow is final negative'];
|
||||
|
||||
}
|
||||
|
||||
private static function buildRegistry(): Registry
|
||||
{
|
||||
$builder = new DefinitionBuilder();
|
||||
$builder
|
||||
->setInitialPlaces(['initial'])
|
||||
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
|
||||
->setMetadataStore(
|
||||
new InMemoryMetadataStore(
|
||||
placesMetadata: [
|
||||
'final_positive' => [
|
||||
'isFinal' => true,
|
||||
'isFinalPositive' => true,
|
||||
],
|
||||
'final_negative' => [
|
||||
'isFinal' => true,
|
||||
'isFinalPositive' => false,
|
||||
],
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
|
||||
$registry = new Registry();
|
||||
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return $registry;
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Entity\DocumentCategory;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Chill\DocStoreBundle\Workflow\AccompanyingCourseDocumentDuplicator;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AccompanyingCourseDocumentDuplicatorTest extends TestCase
|
||||
{
|
||||
public function testDuplicate(): void
|
||||
{
|
||||
$object = new StoredObject();
|
||||
|
||||
$document = new AccompanyingCourseDocument();
|
||||
$document
|
||||
->setDate($date = new \DateTimeImmutable())
|
||||
->setObject($object)
|
||||
->setTitle('Title')
|
||||
->setUser($user = new User())
|
||||
->setCategory($category = new DocumentCategory('bundle', 10))
|
||||
->setDescription($description = 'Description');
|
||||
|
||||
$actual = $this->buildDuplicator()->duplicate($document);
|
||||
|
||||
self::assertSame($date, $actual->getDate());
|
||||
// FYI, the duplication of object is checked by the mock
|
||||
self::assertNotNull($actual->getObject());
|
||||
self::assertStringStartsWith('Title', $actual->getTitle());
|
||||
self::assertSame($user, $actual->getUser());
|
||||
self::assertSame($category, $actual->getCategory());
|
||||
self::assertEquals($description, $actual->getDescription());
|
||||
}
|
||||
|
||||
private function buildDuplicator(): AccompanyingCourseDocumentDuplicator
|
||||
{
|
||||
$storedObjectDuplicate = $this->createMock(StoredObjectDuplicate::class);
|
||||
$storedObjectDuplicate->expects($this->once())->method('duplicate')
|
||||
->with($this->isInstanceOf(StoredObject::class))->willReturn(new StoredObject());
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$translator->method('trans')->withAnyParameters()->willReturn('duplicated');
|
||||
$clock = new MockClock();
|
||||
|
||||
return new AccompanyingCourseDocumentDuplicator(
|
||||
$storedObjectDuplicate,
|
||||
$translator,
|
||||
$clock
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
||||
use Chill\DocStoreBundle\Workflow\AccompanyingCourseDocumentWorkflowHandler;
|
||||
use Chill\DocStoreBundle\Workflow\WorkflowWithPublicViewDocumentHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvidePersonsAssociated;
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvideThirdPartiesAssociated;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class AccompanyingCourseDocumentWorkflowHandlerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testGetSuggestedUsers()
|
||||
{
|
||||
$accompanyingPeriod = new AccompanyingPeriod();
|
||||
$document = new AccompanyingCourseDocument();
|
||||
$document->setCourse($accompanyingPeriod)->setUser($user1 = new User());
|
||||
$accompanyingPeriod->setUser($user = new User());
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow->setRelatedEntityId(1);
|
||||
|
||||
$handler = new AccompanyingCourseDocumentWorkflowHandler(
|
||||
$this->prophesize(TranslatorInterface::class)->reveal(),
|
||||
$this->prophesize(EntityWorkflowRepository::class)->reveal(),
|
||||
$this->buildRepository($document, 1),
|
||||
new WorkflowWithPublicViewDocumentHelper($this->prophesize(Environment::class)->reveal()),
|
||||
$this->prophesize(ProvideThirdPartiesAssociated::class)->reveal(),
|
||||
$this->prophesize(ProvidePersonsAssociated::class)->reveal(),
|
||||
);
|
||||
|
||||
$users = $handler->getSuggestedUsers($entityWorkflow);
|
||||
|
||||
self::assertCount(2, $users);
|
||||
self::assertContains($user, $users);
|
||||
self::assertContains($user1, $users);
|
||||
}
|
||||
|
||||
public function testGetSuggestedUsersWithDuplicates()
|
||||
{
|
||||
$accompanyingPeriod = new AccompanyingPeriod();
|
||||
$document = new AccompanyingCourseDocument();
|
||||
$document->setCourse($accompanyingPeriod)->setUser($user1 = new User());
|
||||
$accompanyingPeriod->setUser($user1);
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
$entityWorkflow->setRelatedEntityId(1);
|
||||
|
||||
$handler = new AccompanyingCourseDocumentWorkflowHandler(
|
||||
$this->prophesize(TranslatorInterface::class)->reveal(),
|
||||
$this->prophesize(EntityWorkflowRepository::class)->reveal(),
|
||||
$this->buildRepository($document, 1),
|
||||
new WorkflowWithPublicViewDocumentHelper($this->prophesize(Environment::class)->reveal()),
|
||||
$this->prophesize(ProvideThirdPartiesAssociated::class)->reveal(),
|
||||
$this->prophesize(ProvidePersonsAssociated::class)->reveal(),
|
||||
);
|
||||
|
||||
$users = $handler->getSuggestedUsers($entityWorkflow);
|
||||
|
||||
self::assertCount(1, $users);
|
||||
self::assertContains($user1, $users);
|
||||
}
|
||||
|
||||
private function buildRepository(AccompanyingCourseDocument $document, int $id): AccompanyingCourseDocumentRepository
|
||||
{
|
||||
$repository = $this->prophesize(AccompanyingCourseDocumentRepository::class);
|
||||
$repository->find($id)->willReturn($document);
|
||||
|
||||
return $repository->reveal();
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Stores the logic to duplicate an AccompanyingCourseDocument associated to a workflow.
|
||||
*/
|
||||
class AccompanyingCourseDocumentDuplicator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StoredObjectDuplicate $storedObjectDuplicate,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
public function duplicate(AccompanyingCourseDocument $document): AccompanyingCourseDocument
|
||||
{
|
||||
$newDoc = new AccompanyingCourseDocument();
|
||||
$newDoc
|
||||
->setCourse($document->getCourse())
|
||||
->setTitle($document->getTitle().' ('.$this->translator->trans('acc_course_document.duplicated_at', ['at' => $this->clock->now()]).')')
|
||||
->setDate($document->getDate())
|
||||
->setDescription($document->getDescription())
|
||||
->setCategory($document->getCategory())
|
||||
->setUser($document->getUser())
|
||||
->setObject($this->storedObjectDuplicate->duplicate($document->getObject()))
|
||||
;
|
||||
|
||||
return $newDoc;
|
||||
}
|
||||
}
|
@@ -16,20 +16,28 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowWithPublicViewInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
||||
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvideThirdPartiesAssociated;
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriod\ProvidePersonsAssociated;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @implements EntityWorkflowWithStoredObjectHandlerInterface<AccompanyingCourseDocument>
|
||||
*/
|
||||
readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface
|
||||
final readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowWithStoredObjectHandlerInterface, EntityWorkflowWithPublicViewInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private EntityWorkflowRepository $workflowRepository,
|
||||
private AccompanyingCourseDocumentRepository $repository,
|
||||
private WorkflowWithPublicViewDocumentHelper $publicViewDocumentHelper,
|
||||
private ProvideThirdPartiesAssociated $thirdPartiesAssociated,
|
||||
private ProvidePersonsAssociated $providePersonsAssociated,
|
||||
) {}
|
||||
|
||||
public function getDeletionRoles(): array
|
||||
@@ -87,12 +95,28 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl
|
||||
|
||||
public function getSuggestedUsers(EntityWorkflow $entityWorkflow): array
|
||||
{
|
||||
$suggestedUsers = $entityWorkflow->getUsersInvolved();
|
||||
$related = $this->getRelatedEntity($entityWorkflow);
|
||||
|
||||
$referrer = $this->getRelatedEntity($entityWorkflow)->getCourse()->getUser();
|
||||
$suggestedUsers[spl_object_hash($referrer)] = $referrer;
|
||||
if (null === $related) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $suggestedUsers;
|
||||
$users = [];
|
||||
if (null !== $user = $related->getUser()) {
|
||||
$users[] = $user;
|
||||
}
|
||||
if (null !== $user = $related->getCourse()->getUser()) {
|
||||
$users[] = $user;
|
||||
}
|
||||
|
||||
return array_values(
|
||||
// filter objects to remove duplicates
|
||||
array_filter(
|
||||
$users,
|
||||
fn ($o, $k) => array_search($o, $users, true) === $k,
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string
|
||||
@@ -136,4 +160,31 @@ readonly class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkfl
|
||||
|
||||
return $this->workflowRepository->findByRelatedEntity(AccompanyingCourseDocument::class, $object->getId());
|
||||
}
|
||||
|
||||
public function renderPublicView(EntityWorkflowSend $entityWorkflowSend, EntityWorkflowViewMetadataDTO $metadata): string
|
||||
{
|
||||
return $this->publicViewDocumentHelper->render($entityWorkflowSend, $metadata, $this);
|
||||
}
|
||||
|
||||
public function getSuggestedPersons(EntityWorkflow $entityWorkflow): array
|
||||
{
|
||||
$related = $this->getRelatedEntity($entityWorkflow);
|
||||
|
||||
if (null === $related) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->providePersonsAssociated->getPersonsAssociated($related->getCourse());
|
||||
}
|
||||
|
||||
public function getSuggestedThirdParties(EntityWorkflow $entityWorkflow): array
|
||||
{
|
||||
$related = $this->getRelatedEntity($entityWorkflow);
|
||||
|
||||
if (null === $related) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->thirdPartiesAssociated->getThirdPartiesAssociated($related->getCourse());
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowWithStoredObjectHandlerInterface;
|
||||
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||
use Twig\Environment;
|
||||
|
||||
class WorkflowWithPublicViewDocumentHelper
|
||||
{
|
||||
public function __construct(private readonly Environment $twig) {}
|
||||
|
||||
public function render(EntityWorkflowSend $send, EntityWorkflowViewMetadataDTO $metadata, EntityWorkflowHandlerInterface&EntityWorkflowWithStoredObjectHandlerInterface $handler): string
|
||||
{
|
||||
$entityWorkflow = $send->getEntityWorkflowStep()->getEntityWorkflow();
|
||||
$storedObject = $handler->getAssociatedStoredObject($entityWorkflow);
|
||||
|
||||
if (null === $storedObject) {
|
||||
return 'document removed';
|
||||
}
|
||||
|
||||
$title = $handler->getEntityTitle($entityWorkflow);
|
||||
|
||||
return $this->twig->render(
|
||||
'@ChillDocStore/Workflow/public_view_with_document_render.html.twig',
|
||||
[
|
||||
'title' => $title,
|
||||
'storedObject' => $storedObject,
|
||||
'send' => $send,
|
||||
'metadata' => $metadata,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@@ -5,5 +5,6 @@ module.exports = function(encore)
|
||||
});
|
||||
encore.addEntry('mod_async_upload', __dirname + '/Resources/public/module/async_upload/index.ts');
|
||||
encore.addEntry('mod_document_action_buttons_group', __dirname + '/Resources/public/module/document_action_buttons_group/index');
|
||||
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index.ts');
|
||||
encore.addEntry('mod_document_download_button', __dirname + '/Resources/public/module/button_download/index');
|
||||
encore.addEntry('vue_document_signature', __dirname + '/Resources/public/vuejs/DocumentSignature/index');
|
||||
};
|
||||
|
@@ -0,0 +1,11 @@
|
||||
acc_course_document:
|
||||
duplicated_at: >-
|
||||
Dupliqué le {at, date, long} à {at, time, short}
|
||||
|
||||
workflow:
|
||||
public_link:
|
||||
doc_shared_by_at_explanation: >-
|
||||
Le document a été partagé avec vous par {byUser}, le {at, date, long} à {at, time, short}.
|
||||
doc_shared_automatically_at_explanation: >-
|
||||
Le document a été partagé avec vous le {at, date, long} à {at, time, short}
|
||||
|
@@ -80,6 +80,10 @@ online_edit_document: Éditer en ligne
|
||||
|
||||
workflow:
|
||||
Document deleted: Document supprimé
|
||||
public_link:
|
||||
shared_doc: Document partagé
|
||||
title: Document partagé
|
||||
main_document: Document principal
|
||||
|
||||
# ROLES
|
||||
accompanyingCourseDocument: Documents dans les parcours d'accompagnement
|
||||
|
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class UserGroupAdminController extends CRUDController
|
||||
{
|
||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||
{
|
||||
$query->addSelect('JSON_EXTRACT(e.label, :lang) AS HIDDEN labeli18n')
|
||||
->setParameter('lang', $request->getLocale());
|
||||
$query->addOrderBy('labeli18n', 'ASC');
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||
|
||||
class UserGroupApiController extends ApiController {}
|
177
src/Bundle/ChillMainBundle/Controller/UserGroupController.php
Normal file
177
src/Bundle/ChillMainBundle/Controller/UserGroupController.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||
use Chill\MainBundle\Repository\UserGroupRepositoryInterface;
|
||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
|
||||
use Chill\MainBundle\Security\Authorization\UserGroupVoter;
|
||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* Controller to see and manage user groups.
|
||||
*/
|
||||
final readonly class UserGroupController
|
||||
{
|
||||
public function __construct(
|
||||
private UserGroupRepositoryInterface $userGroupRepository,
|
||||
private Security $security,
|
||||
private PaginatorFactoryInterface $paginatorFactory,
|
||||
private Environment $twig,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private ChillUrlGeneratorInterface $chillUrlGenerator,
|
||||
private EntityManagerInterface $objectManager,
|
||||
private ChillEntityRenderManagerInterface $chillEntityRenderManager,
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/main/user-groups/my', name: 'chill_main_user_groups_my')]
|
||||
public function myUserGroups(): Response
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$nb = $this->userGroupRepository->countByUser($user);
|
||||
$paginator = $this->paginatorFactory->create($nb);
|
||||
|
||||
$groups = $this->userGroupRepository->findByUser($user, true, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
|
||||
$forms = new \SplObjectStorage();
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$forms->attach($group, $this->createFormAppendUserForGroup($group)?->createView());
|
||||
}
|
||||
|
||||
return new Response($this->twig->render('@ChillMain/UserGroup/my_user_groups.html.twig', [
|
||||
'groups' => $groups,
|
||||
'paginator' => $paginator,
|
||||
'forms' => $forms,
|
||||
]));
|
||||
}
|
||||
|
||||
#[Route('/{_locale}/main/user-groups/{id}/append', name: 'chill_main_user_groups_append_users')]
|
||||
public function appendUsersToGroup(UserGroup $userGroup, Request $request, Session $session): Response
|
||||
{
|
||||
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$form = $this->createFormAppendUserForGroup($userGroup);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
foreach ($form['users']->getData() as $user) {
|
||||
$userGroup->addUser($user);
|
||||
|
||||
$session->getFlashBag()->add(
|
||||
'success',
|
||||
new TranslatableMessage(
|
||||
'user_group.user_added',
|
||||
[
|
||||
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
|
||||
'user' => $this->chillEntityRenderManager->renderString($user, []),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->objectManager->flush();
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
|
||||
);
|
||||
}
|
||||
if ($form->isSubmitted()) {
|
||||
$errors = [];
|
||||
foreach ($form->getErrors() as $error) {
|
||||
$errors[] = $error->getMessage();
|
||||
}
|
||||
|
||||
return new Response(implode(', ', $errors));
|
||||
}
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ParamConverter("user", class=User::class, options={"id" = "userId"})
|
||||
*/
|
||||
#[Route('/{_locale}/main/user-group/{id}/user/{userId}/remove', name: 'chill_main_user_groups_remove_user')]
|
||||
public function removeUserToGroup(UserGroup $userGroup, User $user, Session $session): Response
|
||||
{
|
||||
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $userGroup)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$userGroup->removeUser($user);
|
||||
$this->objectManager->flush();
|
||||
|
||||
$session->getFlashBag()->add(
|
||||
'success',
|
||||
new TranslatableMessage(
|
||||
'user_group.user_removed',
|
||||
[
|
||||
'user_group' => $this->chillEntityRenderManager->renderString($userGroup, []),
|
||||
'user' => $this->chillEntityRenderManager->renderString($user, []),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_user_groups_my')
|
||||
);
|
||||
}
|
||||
|
||||
private function createFormAppendUserForGroup(UserGroup $group): ?FormInterface
|
||||
{
|
||||
if (!$this->security->isGranted(UserGroupVoter::APPEND_TO_GROUP, $group)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$builder = $this->formFactory->createBuilder(FormType::class, ['users' => []], [
|
||||
'action' => $this->chillUrlGenerator->generateWithReturnPath('chill_main_user_groups_append_users', ['id' => $group->getId()]),
|
||||
]);
|
||||
$builder->add('users', PickUserDynamicType::class, [
|
||||
'submit_on_adding_new_entity' => true,
|
||||
'label' => 'user_group.append_users',
|
||||
'mapped' => false,
|
||||
'multiple' => true,
|
||||
]);
|
||||
|
||||
return $builder->getForm();
|
||||
}
|
||||
}
|
@@ -71,7 +71,7 @@ final readonly class WorkflowAddSignatureController
|
||||
|
||||
return new Response(
|
||||
$this->twig->render(
|
||||
'@ChillMain/Workflow/_signature_sign.html.twig',
|
||||
'@ChillMain/Workflow/signature_sign.html.twig',
|
||||
['signature' => $signatureClient]
|
||||
)
|
||||
);
|
||||
|
@@ -300,19 +300,12 @@ class WorkflowController extends AbstractController
|
||||
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
|
||||
// possible transition
|
||||
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||
$usersInvolved = $entityWorkflow->getUsersInvolved();
|
||||
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
|
||||
|
||||
if (false !== $currentUserFound) {
|
||||
unset($usersInvolved[$currentUserFound]);
|
||||
}
|
||||
|
||||
$transitionForm = $this->createForm(
|
||||
WorkflowStepType::class,
|
||||
$stepDTO,
|
||||
[
|
||||
'entity_workflow' => $entityWorkflow,
|
||||
'suggested_users' => $usersInvolved,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -430,7 +423,7 @@ class WorkflowController extends AbstractController
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'@ChillMain/Workflow/_signature_metadata.html.twig',
|
||||
'@ChillMain/Workflow/signature_metadata.html.twig',
|
||||
[
|
||||
'metadata_form' => $metadataForm->createView(),
|
||||
'person' => $signature->getSigner(),
|
||||
|
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
||||
use Chill\MainBundle\Workflow\SignatureStepStateChanger;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class WorkflowSignatureCancelController
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private Security $security,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private Environment $twig,
|
||||
private SignatureStepStateChanger $signatureStepStateChanger,
|
||||
private ChillUrlGeneratorInterface $chillUrlGenerator,
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/main/workflow/signature/{id}/cancel', name: 'chill_main_workflow_signature_cancel')]
|
||||
public function cancelSignature(EntityWorkflowStepSignature $signature, Request $request): Response
|
||||
{
|
||||
return $this->markSignatureAction(
|
||||
$signature,
|
||||
$request,
|
||||
EntityWorkflowStepSignatureVoter::CANCEL,
|
||||
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsCanceled($signature); },
|
||||
'@ChillMain/WorkflowSignature/cancel.html.twig',
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/{_locale}/main/workflow/signature/{id}/reject', name: 'chill_main_workflow_signature_reject')]
|
||||
public function rejectSignature(EntityWorkflowStepSignature $signature, Request $request): Response
|
||||
{
|
||||
return $this->markSignatureAction(
|
||||
$signature,
|
||||
$request,
|
||||
EntityWorkflowStepSignatureVoter::REJECT,
|
||||
function (EntityWorkflowStepSignature $signature) {$this->signatureStepStateChanger->markSignatureAsRejected($signature); },
|
||||
'@ChillMain/WorkflowSignature/reject.html.twig',
|
||||
);
|
||||
}
|
||||
|
||||
private function markSignatureAction(
|
||||
EntityWorkflowStepSignature $signature,
|
||||
Request $request,
|
||||
string $permissionAttribute,
|
||||
callable $markSignature,
|
||||
string $template,
|
||||
): Response {
|
||||
|
||||
if (!$this->security->isGranted($permissionAttribute, $signature)) {
|
||||
throw new AccessDeniedHttpException('not allowed to cancel this signature');
|
||||
}
|
||||
|
||||
$form = $this->formFactory->create();
|
||||
$form->add('confirm', SubmitType::class, ['label' => 'Confirm']);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$markSignature($signature);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->chillUrlGenerator->returnPathOr('chill_main_workflow_show', ['id' => $signature->getStep()->getEntityWorkflow()->getId()])
|
||||
);
|
||||
}
|
||||
|
||||
return
|
||||
new Response(
|
||||
$this->twig->render(
|
||||
$template,
|
||||
['form' => $form->createView(), 'signature' => $signature]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSend;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\Exception\HandlerWithPublicViewNotFoundException;
|
||||
use Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage;
|
||||
use Chill\MainBundle\Workflow\Templating\EntityWorkflowViewMetadataDTO;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class WorkflowViewSendPublicController
|
||||
{
|
||||
public const LOG_PREFIX = '[workflow-view-send-public-controller] ';
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LoggerInterface $chillLogger,
|
||||
private EntityWorkflowManager $entityWorkflowManager,
|
||||
private ClockInterface $clock,
|
||||
private Environment $environment,
|
||||
private MessageBusInterface $messageBus,
|
||||
) {}
|
||||
|
||||
#[Route('/public/main/workflow/send/{uuid}/view/{verificationKey}', name: 'chill_main_workflow_send_view_public', methods: ['GET'])]
|
||||
public function __invoke(EntityWorkflowSend $workflowSend, string $verificationKey, Request $request): Response
|
||||
{
|
||||
if (50 < $workflowSend->getNumberOfErrorTrials()) {
|
||||
throw new AccessDeniedHttpException('number of trials exceeded, no more access allowed');
|
||||
}
|
||||
|
||||
if ($verificationKey !== $workflowSend->getPrivateToken()) {
|
||||
$this->chillLogger->info(self::LOG_PREFIX.'Invalid trial for this send', ['client_ip' => $request->getClientIp()]);
|
||||
$workflowSend->increaseErrorTrials();
|
||||
$this->entityManager->flush();
|
||||
|
||||
throw new AccessDeniedHttpException('invalid verification key');
|
||||
}
|
||||
|
||||
if ($this->clock->now() > $workflowSend->getExpireAt()) {
|
||||
return new Response(
|
||||
$this->environment->render('@ChillMain/Workflow/workflow_view_send_public_expired.html.twig'),
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
if (100 < $workflowSend->getViews()->count()) {
|
||||
$this->chillLogger->info(self::LOG_PREFIX.'100 view reached, not allowed to see it again');
|
||||
throw new AccessDeniedHttpException('100 views reached, not allowed to see it again');
|
||||
}
|
||||
|
||||
try {
|
||||
$metadata = new EntityWorkflowViewMetadataDTO(
|
||||
$workflowSend->getViews()->count(),
|
||||
100 - $workflowSend->getViews()->count(),
|
||||
);
|
||||
$response = new Response(
|
||||
$this->entityWorkflowManager->renderPublicView($workflowSend, $metadata),
|
||||
);
|
||||
|
||||
$view = new EntityWorkflowSendView($workflowSend, $this->clock->now(), $request->getClientIp());
|
||||
$this->entityManager->persist($view);
|
||||
$this->messageBus->dispatch(new PostPublicViewMessage($view->getId()));
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $response;
|
||||
} catch (HandlerWithPublicViewNotFoundException $e) {
|
||||
throw new \RuntimeException('Could not render the public view', previous: $e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
|
||||
class LoadUserGroup extends Fixture implements FixtureGroupInterface
|
||||
{
|
||||
public static function getGroups(): array
|
||||
{
|
||||
return ['user-group'];
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$centerASocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_social']);
|
||||
$centerBSocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_social']);
|
||||
$multiCenter = $manager->getRepository(User::class)->findOneBy(['username' => 'multi_center']);
|
||||
$administrativeA = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_administrative']);
|
||||
$administrativeB = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_administrative']);
|
||||
|
||||
$level1 = $this->generateLevelGroup('Niveau 1', '#eec84aff', '#000000ff', 'level');
|
||||
$level1->addUser($centerASocial)->addUser($centerBSocial);
|
||||
$manager->persist($level1);
|
||||
|
||||
$level2 = $this->generateLevelGroup('Niveau 2', ' #e2793dff', '#000000ff', 'level');
|
||||
$level2->addUser($multiCenter);
|
||||
$manager->persist($level2);
|
||||
|
||||
$level3 = $this->generateLevelGroup('Niveau 3', ' #df4949ff', '#000000ff', 'level');
|
||||
$level3->addUser($multiCenter);
|
||||
$manager->persist($level3);
|
||||
|
||||
$tss = $this->generateLevelGroup('Travailleur sociaux', '#43b29dff', '#000000ff', '');
|
||||
$tss->addUser($multiCenter)->addUser($centerASocial)->addUser($centerBSocial);
|
||||
$manager->persist($tss);
|
||||
$admins = $this->generateLevelGroup('Administratif', '#334d5cff', '#000000ff', '');
|
||||
$admins->addUser($administrativeA)->addUser($administrativeB);
|
||||
$manager->persist($admins);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
private function generateLevelGroup(string $title, string $backgroundColor, string $foregroundColor, string $excludeKey): UserGroup
|
||||
{
|
||||
$userGroup = new UserGroup();
|
||||
|
||||
return $userGroup
|
||||
->setLabel(['fr' => $title])
|
||||
->setBackgroundColor($backgroundColor)
|
||||
->setForegroundColor($foregroundColor)
|
||||
->setExcludeKey($excludeKey)
|
||||
;
|
||||
}
|
||||
}
|
@@ -24,6 +24,8 @@ use Chill\MainBundle\Controller\LocationTypeController;
|
||||
use Chill\MainBundle\Controller\NewsItemController;
|
||||
use Chill\MainBundle\Controller\RegroupmentController;
|
||||
use Chill\MainBundle\Controller\UserController;
|
||||
use Chill\MainBundle\Controller\UserGroupAdminController;
|
||||
use Chill\MainBundle\Controller\UserGroupApiController;
|
||||
use Chill\MainBundle\Controller\UserJobApiController;
|
||||
use Chill\MainBundle\Controller\UserJobController;
|
||||
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
|
||||
@@ -59,6 +61,7 @@ use Chill\MainBundle\Entity\LocationType;
|
||||
use Chill\MainBundle\Entity\NewsItem;
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Form\CenterType;
|
||||
use Chill\MainBundle\Form\CivilityType;
|
||||
@@ -68,6 +71,7 @@ use Chill\MainBundle\Form\LocationFormType;
|
||||
use Chill\MainBundle\Form\LocationTypeType;
|
||||
use Chill\MainBundle\Form\NewsItemType;
|
||||
use Chill\MainBundle\Form\RegroupmentType;
|
||||
use Chill\MainBundle\Form\UserGroupType;
|
||||
use Chill\MainBundle\Form\UserJobType;
|
||||
use Chill\MainBundle\Form\UserType;
|
||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
||||
@@ -353,6 +357,28 @@ class ChillMainExtension extends Extension implements
|
||||
{
|
||||
$container->prependExtensionConfig('chill_main', [
|
||||
'cruds' => [
|
||||
[
|
||||
'class' => UserGroup::class,
|
||||
'controller' => UserGroupAdminController::class,
|
||||
'name' => 'admin_user_group',
|
||||
'base_path' => '/admin/main/user-group',
|
||||
'base_role' => 'ROLE_ADMIN',
|
||||
'form_class' => UserGroupType::class,
|
||||
'actions' => [
|
||||
'index' => [
|
||||
'role' => 'ROLE_ADMIN',
|
||||
'template' => '@ChillMain/UserGroup/index.html.twig',
|
||||
],
|
||||
'new' => [
|
||||
'role' => 'ROLE_ADMIN',
|
||||
'template' => '@ChillMain/UserGroup/new.html.twig',
|
||||
],
|
||||
'edit' => [
|
||||
'role' => 'ROLE_ADMIN',
|
||||
'template' => '@ChillMain/UserGroup/edit.html.twig',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => UserJob::class,
|
||||
'controller' => UserJobController::class,
|
||||
@@ -803,6 +829,21 @@ class ChillMainExtension extends Extension implements
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'class' => UserGroup::class,
|
||||
'controller' => UserGroupApiController::class,
|
||||
'name' => 'user-group',
|
||||
'base_path' => '/api/1.0/main/user-group',
|
||||
'base_role' => 'ROLE_USER',
|
||||
'actions' => [
|
||||
'_index' => [
|
||||
'methods' => [
|
||||
Request::METHOD_GET => true,
|
||||
Request::METHOD_HEAD => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
244
src/Bundle/ChillMainBundle/Entity/UserGroup.php
Normal file
244
src/Bundle/ChillMainBundle/Entity/UserGroup.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_main_user_group')]
|
||||
// this discriminator key is required for automated denormalization
|
||||
#[DiscriminatorMap('type', mapping: ['user_group' => UserGroup::class])]
|
||||
class UserGroup
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
|
||||
private bool $active = true;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||
private array $label = [];
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>&Selectable<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_user_group_user')]
|
||||
private Collection&Selectable $users;
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>&Selectable<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_user_group_user_admin')]
|
||||
private Collection&Selectable $adminUsers;
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])]
|
||||
private string $backgroundColor = '#ffffffff';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#000000ff'])]
|
||||
private string $foregroundColor = '#000000ff';
|
||||
|
||||
/**
|
||||
* Groups with same exclude key are mutually exclusive: adding one in a many-to-one relationship
|
||||
* will exclude others.
|
||||
*
|
||||
* An empty string means "no exclusion"
|
||||
*/
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
private string $excludeKey = '';
|
||||
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
#[Assert\Email]
|
||||
private string $email = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->adminUsers = new ArrayCollection();
|
||||
$this->users = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
public function setActive(bool $active): self
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addAdminUser(User $user): self
|
||||
{
|
||||
if (!$this->adminUsers->contains($user)) {
|
||||
$this->adminUsers[] = $user;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeAdminUser(User $user): self
|
||||
{
|
||||
$this->adminUsers->removeElement($user);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addUser(User $user): self
|
||||
{
|
||||
if (!$this->users->contains($user)) {
|
||||
$this->users[] = $user;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeUser(User $user): self
|
||||
{
|
||||
if ($this->users->contains($user)) {
|
||||
$this->users->removeElement($user);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getLabel(): array
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Selectable<int, User>&Collection<int, User>
|
||||
*/
|
||||
public function getUsers(): Collection&Selectable
|
||||
{
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Selectable<int, User>&Collection<int, User>
|
||||
*/
|
||||
public function getAdminUsers(): Collection&Selectable
|
||||
{
|
||||
return $this->adminUsers;
|
||||
}
|
||||
|
||||
public function getForegroundColor(): string
|
||||
{
|
||||
return $this->foregroundColor;
|
||||
}
|
||||
|
||||
public function getExcludeKey(): string
|
||||
{
|
||||
return $this->excludeKey;
|
||||
}
|
||||
|
||||
public function getBackgroundColor(): string
|
||||
{
|
||||
return $this->backgroundColor;
|
||||
}
|
||||
|
||||
public function setForegroundColor(string $foregroundColor): self
|
||||
{
|
||||
$this->foregroundColor = $foregroundColor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setBackgroundColor(string $backgroundColor): self
|
||||
{
|
||||
$this->backgroundColor = $backgroundColor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setExcludeKey(string $excludeKey): self
|
||||
{
|
||||
$this->excludeKey = $excludeKey;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLabel(array $label): self
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): self
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasEmail(): bool
|
||||
{
|
||||
return '' !== $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current object is an instance of the UserGroup class.
|
||||
*
|
||||
* In use in twig template, to discriminate when there an object can be polymorphic.
|
||||
*
|
||||
* @return bool returns true if the current object is an instance of UserGroup, false otherwise
|
||||
*/
|
||||
public function isUserGroup(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function contains(User $user): bool
|
||||
{
|
||||
return $this->users->contains($user);
|
||||
}
|
||||
|
||||
public function getUserListByLabelAscending(): ReadableCollection
|
||||
{
|
||||
$criteria = Criteria::create();
|
||||
$criteria->orderBy(['label' => Order::Ascending]);
|
||||
|
||||
return $this->getUsers()->matching($criteria);
|
||||
}
|
||||
|
||||
public function getAdminUserListByLabelAscending(): ReadableCollection
|
||||
{
|
||||
$criteria = Criteria::create();
|
||||
$criteria->orderBy(['label' => Order::Ascending]);
|
||||
|
||||
return $this->getAdminUsers()->matching($criteria);
|
||||
}
|
||||
}
|
@@ -442,18 +442,18 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
$newStep->addCcUser($user);
|
||||
}
|
||||
|
||||
foreach ($transitionContextDTO->futureDestUsers as $user) {
|
||||
foreach ($transitionContextDTO->getFutureDestUsers() as $user) {
|
||||
$newStep->addDestUser($user);
|
||||
}
|
||||
|
||||
foreach ($transitionContextDTO->getFutureDestUserGroups() as $userGroup) {
|
||||
$newStep->addDestUserGroup($userGroup);
|
||||
}
|
||||
|
||||
if (null !== $transitionContextDTO->futureUserSignature) {
|
||||
$newStep->addDestUser($transitionContextDTO->futureUserSignature);
|
||||
}
|
||||
|
||||
foreach ($transitionContextDTO->futureDestEmails as $email) {
|
||||
$newStep->addDestEmail($email);
|
||||
}
|
||||
|
||||
if (null !== $transitionContextDTO->futureUserSignature) {
|
||||
new EntityWorkflowStepSignature($newStep, $transitionContextDTO->futureUserSignature);
|
||||
} else {
|
||||
@@ -462,6 +462,13 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($transitionContextDTO->futureDestineeThirdParties as $thirdParty) {
|
||||
new EntityWorkflowSend($newStep, $thirdParty, $transitionAt->add(new \DateInterval('P30D')));
|
||||
}
|
||||
foreach ($transitionContextDTO->futureDestineeEmails as $email) {
|
||||
new EntityWorkflowSend($newStep, $email, $transitionAt->add(new \DateInterval('P30D')));
|
||||
}
|
||||
|
||||
// copy the freeze
|
||||
if ($this->isFreeze()) {
|
||||
$newStep->setFreezeAfter(true);
|
||||
|
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Random\Randomizer;
|
||||
|
||||
/**
|
||||
* An entity which stores then sending of a workflow's content to
|
||||
* some external entity.
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_main_workflow_entity_send')]
|
||||
class EntityWorkflowSend implements TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: ThirdParty::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?ThirdParty $destineeThirdParty = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
private string $destineeEmail = '';
|
||||
|
||||
#[ORM\Column(type: 'uuid', unique: true, nullable: false)]
|
||||
private UuidInterface $uuid;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255, nullable: false)]
|
||||
private string $privateToken;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: false, options: ['default' => 0])]
|
||||
private int $numberOfErrorTrials = 0;
|
||||
|
||||
/**
|
||||
* @var Collection<int, EntityWorkflowSendView>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: EntityWorkflowSendView::class, mappedBy: 'send')]
|
||||
private Collection $views;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'sends')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private EntityWorkflowStep $entityWorkflowStep,
|
||||
string|ThirdParty $destinee,
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
|
||||
private \DateTimeImmutable $expireAt,
|
||||
) {
|
||||
$this->uuid = Uuid::uuid4();
|
||||
$random = new Randomizer();
|
||||
$this->privateToken = bin2hex($random->getBytes(48));
|
||||
|
||||
$this->entityWorkflowStep->addSend($this);
|
||||
|
||||
if ($destinee instanceof ThirdParty) {
|
||||
$this->destineeThirdParty = $destinee;
|
||||
} else {
|
||||
$this->destineeEmail = $destinee;
|
||||
}
|
||||
|
||||
$this->views = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use the @see{EntityWorkflowSendView}'s constructor instead
|
||||
*/
|
||||
public function addView(EntityWorkflowSendView $view): self
|
||||
{
|
||||
if (!$this->views->contains($view)) {
|
||||
$this->views->add($view);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDestineeEmail(): string
|
||||
{
|
||||
return $this->destineeEmail;
|
||||
}
|
||||
|
||||
public function getDestineeThirdParty(): ?ThirdParty
|
||||
{
|
||||
return $this->destineeThirdParty;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getNumberOfErrorTrials(): int
|
||||
{
|
||||
return $this->numberOfErrorTrials;
|
||||
}
|
||||
|
||||
public function getPrivateToken(): string
|
||||
{
|
||||
return $this->privateToken;
|
||||
}
|
||||
|
||||
public function getEntityWorkflowStep(): EntityWorkflowStep
|
||||
{
|
||||
return $this->entityWorkflowStep;
|
||||
}
|
||||
|
||||
public function getEntityWorkflowStepChained(): ?EntityWorkflowStep
|
||||
{
|
||||
foreach ($this->getEntityWorkflowStep()->getEntityWorkflow()->getStepsChained() as $step) {
|
||||
if ($this->getEntityWorkflowStep() === $step) {
|
||||
return $step;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getUuid(): UuidInterface
|
||||
{
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
public function getExpireAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->expireAt;
|
||||
}
|
||||
|
||||
public function getViews(): Collection
|
||||
{
|
||||
return $this->views;
|
||||
}
|
||||
|
||||
public function increaseErrorTrials(): void
|
||||
{
|
||||
$this->numberOfErrorTrials = $this->numberOfErrorTrials + 1;
|
||||
}
|
||||
|
||||
public function getDestinee(): string|ThirdParty
|
||||
{
|
||||
if (null !== $this->getDestineeThirdParty()) {
|
||||
return $this->getDestineeThirdParty();
|
||||
}
|
||||
|
||||
return $this->getDestineeEmail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the kind of destinee based on whether the destinee is a thirdParty or an emailAddress.
|
||||
*
|
||||
* @return 'thirdParty'|'email' 'thirdParty' if the destinee is a third party, 'email' otherwise
|
||||
*/
|
||||
public function getDestineeKind(): string
|
||||
{
|
||||
if (null !== $this->getDestineeThirdParty()) {
|
||||
return 'thirdParty';
|
||||
}
|
||||
|
||||
return 'email';
|
||||
}
|
||||
|
||||
public function isViewed(): bool
|
||||
{
|
||||
return $this->views->count() > 0;
|
||||
}
|
||||
|
||||
public function isExpired(?\DateTimeImmutable $now = null): bool
|
||||
{
|
||||
return ($now ?? new \DateTimeImmutable('now')) >= $this->expireAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the most recent view.
|
||||
*
|
||||
* @return EntityWorkflowSendView|null returns the last view or null if there are no views
|
||||
*/
|
||||
public function getLastView(): ?EntityWorkflowSendView
|
||||
{
|
||||
$last = null;
|
||||
foreach ($this->views as $view) {
|
||||
if (null === $last) {
|
||||
$last = $view;
|
||||
} else {
|
||||
if ($view->getViewAt() > $last->getViewAt()) {
|
||||
$last = $view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array of views grouped by their remote IP address.
|
||||
*
|
||||
* @return array<string, list<EntityWorkflowSendView>> an associative array where the keys are IP addresses and the values are arrays of views associated with those IPs
|
||||
*/
|
||||
public function getViewsByIp(): array
|
||||
{
|
||||
$views = [];
|
||||
|
||||
foreach ($this->getViews() as $view) {
|
||||
$views[$view->getRemoteIp()][] = $view;
|
||||
}
|
||||
|
||||
return $views;
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* Register the viewing action from an external destinee.
|
||||
*/
|
||||
#[ORM\Entity(readOnly: true)]
|
||||
#[ORM\Table(name: 'chill_main_workflow_entity_send_views')]
|
||||
class EntityWorkflowSendView
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
private ?int $id = null;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: EntityWorkflowSend::class, inversedBy: 'views')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private EntityWorkflowSend $send,
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private \DateTimeInterface $viewAt,
|
||||
#[ORM\Column(type: Types::TEXT)]
|
||||
private string $remoteIp = '',
|
||||
) {
|
||||
$this->send->addView($this);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRemoteIp(): string
|
||||
{
|
||||
return $this->remoteIp;
|
||||
}
|
||||
|
||||
public function getSend(): EntityWorkflowSend
|
||||
{
|
||||
return $this->send;
|
||||
}
|
||||
|
||||
public function getViewAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->viewAt;
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -48,6 +49,13 @@ class EntityWorkflowStep
|
||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
|
||||
private Collection $destUser;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserGroup>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_group')]
|
||||
private Collection $destUserGroups;
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
@@ -104,13 +112,21 @@ class EntityWorkflowStep
|
||||
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepHold::class)]
|
||||
private Collection $holdsOnStep;
|
||||
|
||||
/**
|
||||
* @var Collection<int, EntityWorkflowSend>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
private Collection $sends;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ccUser = new ArrayCollection();
|
||||
$this->destUser = new ArrayCollection();
|
||||
$this->destUserGroups = new ArrayCollection();
|
||||
$this->destUserByAccessKey = new ArrayCollection();
|
||||
$this->signatures = new ArrayCollection();
|
||||
$this->holdsOnStep = new ArrayCollection();
|
||||
$this->sends = new ArrayCollection();
|
||||
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
||||
}
|
||||
|
||||
@@ -123,6 +139,9 @@ class EntityWorkflowStep
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public function addDestEmail(string $email): self
|
||||
{
|
||||
if (!\in_array($email, $this->destEmail, true)) {
|
||||
@@ -141,6 +160,22 @@ class EntityWorkflowStep
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDestUserGroup(UserGroup $userGroup): self
|
||||
{
|
||||
if (!$this->destUserGroups->contains($userGroup)) {
|
||||
$this->destUserGroups[] = $userGroup;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeDestUserGroup(UserGroup $userGroup): self
|
||||
{
|
||||
$this->destUserGroups->removeElement($userGroup);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDestUserByAccessKey(User $user): self
|
||||
{
|
||||
if (!$this->destUserByAccessKey->contains($user) && !$this->destUser->contains($user)) {
|
||||
@@ -162,6 +197,18 @@ class EntityWorkflowStep
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal use @see{EntityWorkflowSend}'s constructor instead
|
||||
*/
|
||||
public function addSend(EntityWorkflowSend $send): self
|
||||
{
|
||||
if (!$this->sends->contains($send)) {
|
||||
$this->sends[] = $send;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
||||
{
|
||||
if ($this->signatures->contains($signature)) {
|
||||
@@ -178,7 +225,9 @@ class EntityWorkflowStep
|
||||
|
||||
/**
|
||||
* get all the users which are allowed to apply a transition: those added manually, and
|
||||
* those added automatically bu using an access key.
|
||||
* those added automatically by using an access key.
|
||||
*
|
||||
* This method exclude the users associated with user groups
|
||||
*
|
||||
* @psalm-suppress DuplicateArrayKey
|
||||
*/
|
||||
@@ -192,6 +241,14 @@ class EntityWorkflowStep
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, UserGroup>
|
||||
*/
|
||||
public function getDestUserGroups(): Collection
|
||||
{
|
||||
return $this->destUserGroups;
|
||||
}
|
||||
|
||||
public function getCcUser(): Collection
|
||||
{
|
||||
return $this->ccUser;
|
||||
@@ -207,6 +264,11 @@ class EntityWorkflowStep
|
||||
return $this->currentStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public function getDestEmail(): array
|
||||
{
|
||||
return $this->destEmail;
|
||||
@@ -241,6 +303,14 @@ class EntityWorkflowStep
|
||||
return $this->signatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, EntityWorkflowSend>
|
||||
*/
|
||||
public function getSends(): Collection
|
||||
{
|
||||
return $this->sends;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
|
@@ -161,6 +161,16 @@ class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdate
|
||||
return EntityWorkflowSignatureStateEnum::PENDING == $this->getState();
|
||||
}
|
||||
|
||||
public function isCanceled(): bool
|
||||
{
|
||||
return EntityWorkflowSignatureStateEnum::CANCELED === $this->getState();
|
||||
}
|
||||
|
||||
public function isRejected(): bool
|
||||
{
|
||||
return EntityWorkflowSignatureStateEnum::REJECTED === $this->getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether all signatures associated with a given workflow step are not pending.
|
||||
*
|
||||
|
@@ -36,6 +36,7 @@ class ChillCollectionType extends AbstractType
|
||||
$view->vars['identifier'] = $options['identifier'];
|
||||
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
|
||||
$view->vars['js_caller'] = $options['js_caller'];
|
||||
$view->vars['uniqid'] = uniqid();
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
|
@@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Form\Type\DataTransformer;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Serializer\Normalizer\DiscriminatedObjectDenormalizer;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
@@ -26,10 +28,14 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
||||
|
||||
public function reverseTransform($value)
|
||||
{
|
||||
if ('' === $value) {
|
||||
if (false === $this->multiple && '' === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->multiple && [] === $value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$denormalized = json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if ($this->multiple) {
|
||||
@@ -74,15 +80,23 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
||||
'user' => User::class,
|
||||
'person' => Person::class,
|
||||
'thirdparty' => ThirdParty::class,
|
||||
'user_group' => UserGroup::class,
|
||||
'user_group_or_user' => DiscriminatedObjectDenormalizer::TYPE,
|
||||
default => throw new \UnexpectedValueException('This type is not supported'),
|
||||
};
|
||||
|
||||
$context = [AbstractNormalizer::GROUPS => ['read']];
|
||||
|
||||
if ('user_group_or_user' === $this->type) {
|
||||
$context[DiscriminatedObjectDenormalizer::ALLOWED_TYPES] = [UserGroup::class, User::class];
|
||||
}
|
||||
|
||||
return
|
||||
$this->denormalizer->denormalize(
|
||||
['type' => $item['type'], 'id' => $item['id']],
|
||||
$class,
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read']],
|
||||
$context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -18,16 +18,30 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Pick user dymically, using vuejs module "AddPerson".
|
||||
*
|
||||
* Possible options:
|
||||
*
|
||||
* - `multiple`: pick one or more users
|
||||
* - `suggested`: a list of suggested users
|
||||
* - `suggest_myself`: append the current user to the list of suggested
|
||||
* - `as_id`: only the id will be set in the returned data
|
||||
* - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked
|
||||
*/
|
||||
class PickUserDynamicType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerInterface $serializer, private readonly NormalizerInterface $normalizer) {}
|
||||
public function __construct(
|
||||
private readonly DenormalizerInterface $denormalizer,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
@@ -46,6 +60,12 @@ class PickUserDynamicType extends AbstractType
|
||||
foreach ($options['suggested'] as $user) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||
}
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof User) {
|
||||
if (true === $options['suggest_myself'] && !in_array($user, $options['suggested'], true)) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
@@ -54,6 +74,8 @@ class PickUserDynamicType extends AbstractType
|
||||
->setDefault('multiple', false)
|
||||
->setAllowedTypes('multiple', ['bool'])
|
||||
->setDefault('compound', false)
|
||||
->setDefault('suggest_myself', false)
|
||||
->setAllowedTypes('suggest_myself', ['bool'])
|
||||
->setDefault('suggested', [])
|
||||
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
||||
->setDefault('as_id', false)
|
||||
|
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Entity which picks a user **or** a user group.
|
||||
*/
|
||||
final class PickUserGroupOrUserDynamicType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DenormalizerInterface $denormalizer,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user_group_or_user'));
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['multiple'] = $options['multiple'];
|
||||
$view->vars['types'] = ['user-group', 'user'];
|
||||
$view->vars['uniqid'] = uniqid('pick_usergroup_dyn');
|
||||
$view->vars['suggested'] = [];
|
||||
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
|
||||
$view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0';
|
||||
|
||||
foreach ($options['suggested'] as $userGroup) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($userGroup, 'json', ['groups' => 'read']);
|
||||
}
|
||||
$user = $this->security->getUser();
|
||||
if ($user instanceof User) {
|
||||
if (true === $options['suggest_myself'] && !in_array($user, $options['suggested'], true)) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setDefault('multiple', false)
|
||||
->setAllowedTypes('multiple', ['bool'])
|
||||
->setDefault('compound', false)
|
||||
->setDefault('suggested', [])
|
||||
->setDefault('suggest_myself', false)
|
||||
->setAllowedTypes('suggest_myself', ['bool'])
|
||||
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
||||
->setDefault('as_id', false)
|
||||
->setAllowedTypes('as_id', ['bool'])
|
||||
->setDefault('submit_on_adding_new_entity', false)
|
||||
->setAllowedTypes('submit_on_adding_new_entity', ['bool']);
|
||||
}
|
||||
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'pick_entity_dynamic';
|
||||
}
|
||||
}
|
64
src/Bundle/ChillMainBundle/Form/UserGroupType.php
Normal file
64
src/Bundle/ChillMainBundle/Form/UserGroupType.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ColorType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class UserGroupType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('label', TranslatableStringFormType::class, [
|
||||
'label' => 'user_group.Label',
|
||||
'required' => true,
|
||||
])
|
||||
->add('active')
|
||||
->add('backgroundColor', ColorType::class, [
|
||||
'label' => 'user_group.BackgroundColor',
|
||||
])
|
||||
->add('foregroundColor', ColorType::class, [
|
||||
'label' => 'user_group.ForegroundColor',
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'user_group.Email',
|
||||
'help' => 'user_group.EmailHelp',
|
||||
'empty_data' => '',
|
||||
])
|
||||
->add('excludeKey', TextType::class, [
|
||||
'label' => 'user_group.ExcludeKey',
|
||||
'help' => 'user_group.ExcludeKeyHelp',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
])
|
||||
->add('users', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.Users',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
])
|
||||
->add('adminUsers', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.adminUsers',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
'help' => 'user_group.adminUsersHelp',
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
@@ -15,19 +15,18 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
|
||||
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Constraints\Email;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Constraints\NotNull;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
@@ -36,6 +35,7 @@ class WorkflowStepType extends AbstractType
|
||||
public function __construct(
|
||||
private readonly Registry $registry,
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
@@ -45,6 +45,9 @@ class WorkflowStepType extends AbstractType
|
||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||
$place = $workflow->getMarking($entityWorkflow);
|
||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
|
||||
$suggestedUsers = $this->entityWorkflowManager->getSuggestedUsers($entityWorkflow);
|
||||
$suggestedThirdParties = $this->entityWorkflowManager->getSuggestedThirdParties($entityWorkflow);
|
||||
$suggestedPersons = $this->entityWorkflowManager->getSuggestedPersons($entityWorkflow);
|
||||
|
||||
if (null === $options['entity_workflow']) {
|
||||
throw new \LogicException('if transition is true, entity_workflow should be defined');
|
||||
@@ -86,7 +89,6 @@ class WorkflowStepType extends AbstractType
|
||||
$builder
|
||||
->add('transition', ChoiceType::class, [
|
||||
'label' => 'workflow.Next step',
|
||||
'mapped' => false,
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'choices' => $choices,
|
||||
@@ -104,6 +106,7 @@ class WorkflowStepType extends AbstractType
|
||||
$toFinal = true;
|
||||
$isForward = 'neutral';
|
||||
$isSignature = [];
|
||||
$isSentExternal = false;
|
||||
|
||||
$metadata = $workflow->getMetadataStore()->getTransitionMetadata($transition);
|
||||
|
||||
@@ -127,6 +130,8 @@ class WorkflowStepType extends AbstractType
|
||||
if (\array_key_exists('isSignature', $meta)) {
|
||||
$isSignature = $meta['isSignature'];
|
||||
}
|
||||
|
||||
$isSentExternal = $isSentExternal ? true : $meta['isSentExternal'] ?? false;
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -134,6 +139,7 @@ class WorkflowStepType extends AbstractType
|
||||
'data-to-final' => $toFinal ? '1' : '0',
|
||||
'data-is-forward' => $isForward,
|
||||
'data-is-signature' => json_encode($isSignature),
|
||||
'data-is-sent-external' => $isSentExternal ? '1' : '0',
|
||||
];
|
||||
},
|
||||
])
|
||||
@@ -151,39 +157,49 @@ class WorkflowStepType extends AbstractType
|
||||
'label' => 'workflow.signature_zone.person signatures',
|
||||
'multiple' => true,
|
||||
'empty_data' => '[]',
|
||||
'suggested' => $suggestedPersons,
|
||||
])
|
||||
->add('futureUserSignature', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.signature_zone.user signature',
|
||||
'multiple' => false,
|
||||
'suggest_myself' => true,
|
||||
'suggested' => $suggestedUsers,
|
||||
])
|
||||
->add('futureDestUsers', PickUserDynamicType::class, [
|
||||
->add('futureDestUsers', PickUserGroupOrUserDynamicType::class, [
|
||||
'label' => 'workflow.dest for next steps',
|
||||
'multiple' => true,
|
||||
'empty_data' => '[]',
|
||||
'suggested' => $options['suggested_users'],
|
||||
'suggested' => $suggestedUsers,
|
||||
'suggest_myself' => true,
|
||||
])
|
||||
->add('futureCcUsers', PickUserDynamicType::class, [
|
||||
'label' => 'workflow.cc for next steps',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'suggested' => $options['suggested_users'],
|
||||
'suggested' => $suggestedUsers,
|
||||
'empty_data' => '[]',
|
||||
'attr' => ['class' => 'future-cc-users'],
|
||||
'suggest_myself' => true,
|
||||
])
|
||||
->add('futureDestEmails', ChillCollectionType::class, [
|
||||
'label' => 'workflow.dest by email',
|
||||
'help' => 'workflow.dest by email help',
|
||||
'allow_add' => true,
|
||||
->add('futureDestineeEmails', ChillCollectionType::class, [
|
||||
'entry_type' => EmailType::class,
|
||||
'button_add_label' => 'workflow.Add an email',
|
||||
'button_remove_label' => 'workflow.Remove an email',
|
||||
'empty_collection_explain' => 'workflow.Any email',
|
||||
'entry_options' => [
|
||||
'constraints' => [
|
||||
new NotNull(), new NotBlank(), new Email(),
|
||||
],
|
||||
'label' => 'Email',
|
||||
'empty_data' => '',
|
||||
],
|
||||
'allow_add' => true,
|
||||
'allow_delete' => true,
|
||||
'delete_empty' => static fn (?string $email) => '' === $email || null === $email,
|
||||
'button_add_label' => 'workflow.transition_destinee_add_emails',
|
||||
'button_remove_label' => 'workflow.transition_destinee_remove_emails',
|
||||
'help' => 'workflow.transition_destinee_emails_help',
|
||||
'label' => 'workflow.transition_destinee_emails_label',
|
||||
])
|
||||
->add('futureDestineeThirdParties', PickThirdpartyDynamicType::class, [
|
||||
'label' => 'workflow.transition_destinee_third_party',
|
||||
'help' => 'workflow.transition_destinee_third_party_help',
|
||||
'multiple' => true,
|
||||
'empty_data' => [],
|
||||
'suggested' => $suggestedThirdParties,
|
||||
]);
|
||||
|
||||
$builder
|
||||
@@ -199,40 +215,6 @@ class WorkflowStepType extends AbstractType
|
||||
$resolver
|
||||
->setDefault('data_class', WorkflowTransitionContextDTO::class)
|
||||
->setRequired('entity_workflow')
|
||||
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
|
||||
->setDefault('suggested_users', [])
|
||||
->setDefault('constraints', [
|
||||
new Callback(
|
||||
function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) {
|
||||
$workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName());
|
||||
$transition = $step->transition;
|
||||
$toFinal = true;
|
||||
|
||||
if (null === $transition) {
|
||||
$context
|
||||
->buildViolation('workflow.You must select a next step, pick another decision if no next steps are available');
|
||||
} else {
|
||||
foreach ($transition->getTos() as $to) {
|
||||
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
|
||||
|
||||
if (
|
||||
!\array_key_exists('isFinal', $meta) || false === $meta['isFinal']
|
||||
) {
|
||||
$toFinal = false;
|
||||
}
|
||||
}
|
||||
$destUsers = $step->futureDestUsers;
|
||||
$destEmails = $step->futureDestEmails;
|
||||
|
||||
if (!$toFinal && [] === $destUsers && [] === $destEmails) {
|
||||
$context
|
||||
->buildViolation('workflow.You must add at least one dest user or email')
|
||||
->atPath('future_dest_users')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
]);
|
||||
->setAllowedTypes('entity_workflow', EntityWorkflow::class);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSendView;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @template-implements ObjectRepository<EntityWorkflowSendView>
|
||||
*/
|
||||
class EntityWorkflowSendViewRepository implements ObjectRepository
|
||||
{
|
||||
private readonly ObjectRepository $repository;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
$this->repository = $registry->getRepository($this->getClassName());
|
||||
}
|
||||
|
||||
public function find($id): ?EntityWorkflowSendView
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findAll()
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?EntityWorkflowSendView
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return EntityWorkflowSendView::class;
|
||||
}
|
||||
}
|
133
src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php
Normal file
133
src/Bundle/ChillMainBundle/Repository/UserGroupRepository.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
|
||||
final class UserGroupRepository implements UserGroupRepositoryInterface, LocaleAwareInterface
|
||||
{
|
||||
private readonly EntityRepository $repository;
|
||||
|
||||
private string $locale;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->repository = $em->getRepository(UserGroup::class);
|
||||
}
|
||||
|
||||
public function setLocale(string $locale): void
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
public function find($id): ?UserGroup
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?UserGroup
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return UserGroup::class;
|
||||
}
|
||||
|
||||
public function provideSearchApiQuery(string $pattern, string $lang, string $selectKey = 'user-group'): SearchApiQuery
|
||||
{
|
||||
$query = new SearchApiQuery();
|
||||
$query
|
||||
->setSelectKey($selectKey)
|
||||
->setSelectJsonbMetadata("jsonb_build_object('id', ug.id)")
|
||||
->setSelectPertinence('3 + SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) + CASE WHEN (EXISTS(SELECT 1 FROM unnest(string_to_array(label->>?, \' \')) AS t WHERE LOWER(t) LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\')) THEN 100 ELSE 0 END', [$pattern, $lang, $lang, $pattern])
|
||||
->setFromClause('chill_main_user_group AS ug')
|
||||
->setWhereClauses('
|
||||
ug.active AND (
|
||||
SIMILARITY(LOWER(UNACCENT(?)), ug.label->>?) > 0.15
|
||||
OR ug.label->>? LIKE \'%\' || LOWER(UNACCENT(?)) || \'%\')
|
||||
', [$pattern, $lang, $pattern, $lang]);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function findByUser(User $user, bool $onlyActive = true, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
$qb = $this->buildQueryByUser($user, $onlyActive);
|
||||
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
||||
if (null !== $offset) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
// ordering thing
|
||||
$qb->addSelect('JSON_EXTRACT(ug.label, :lang) AS HIDDEN label_ordering')
|
||||
->addOrderBy('label_ordering', 'ASC')
|
||||
->setParameter('lang', $this->getLocale());
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function countByUser(User $user, bool $onlyActive = true): int
|
||||
{
|
||||
$qb = $this->buildQueryByUser($user, $onlyActive);
|
||||
$qb->select('count(ug)');
|
||||
|
||||
return $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function buildQueryByUser(User $user, bool $onlyActive): \Doctrine\ORM\QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('ug');
|
||||
$qb->where(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':user', 'ug.users'),
|
||||
$qb->expr()->isMemberOf(':user', 'ug.adminUsers')
|
||||
)
|
||||
);
|
||||
|
||||
$qb->setParameter('user', $user);
|
||||
|
||||
if ($onlyActive) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->eq('ug.active', ':active')
|
||||
);
|
||||
$qb->setParameter('active', true);
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @template-extends ObjectRepository<UserGroup>
|
||||
*/
|
||||
interface UserGroupRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
/**
|
||||
* Provide a SearchApiQuery for searching amongst user groups.
|
||||
*/
|
||||
public function provideSearchApiQuery(string $pattern, string $lang, string $selectKey = 'user-group'): SearchApiQuery;
|
||||
|
||||
public function findByUser(User $user, bool $onlyActive = true, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function countByUser(User $user, bool $onlyActive = true): int;
|
||||
}
|
@@ -12,7 +12,9 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@@ -203,27 +205,29 @@ class EntityWorkflowRepository implements ObjectRepository
|
||||
*
|
||||
* @param \DateTimeImmutable $olderThanDate the date to compare against
|
||||
*
|
||||
* @return list<int> the list of workflow IDs that meet the criteria
|
||||
* @return iterable<EntityWorkflow>
|
||||
*/
|
||||
public function findWorkflowsWithoutFinalStepAndOlderThan(\DateTimeImmutable $olderThanDate): array
|
||||
public function findWorkflowsWithoutFinalStepAndOlderThan(\DateTimeImmutable $olderThanDate): iterable
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('sw');
|
||||
|
||||
$qb->select('sw.id')
|
||||
$qb->select('sw')
|
||||
// only the workflow which are not finalized
|
||||
->where('NOT EXISTS (SELECT 1 FROM chill_main_entity_workflow_step ews WHERE ews.isFinal = TRUE AND ews.entityWorkflow = sw.id)')
|
||||
->where(sprintf('NOT EXISTS (SELECT 1 FROM %s ews WHERE ews.isFinal = TRUE AND ews.entityWorkflow = sw.id)', EntityWorkflowStep::class))
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
// only the workflow where all the last transition is older than transitionAt
|
||||
':olderThanDate > ALL (SELECT ews.transitionAt FROM chill_main_entity_workflow_step ews WHERE ews.transitionAt IS NOT NULL AND ews.entityWorkflow = sw.id)',
|
||||
// only the workflow where all the transitions are older than transitionAt
|
||||
sprintf(':olderThanDate > ALL (SELECT ews2.transitionAt FROM %s ews2 WHERE ews2.transitionAt IS NOT NULL AND ews2.entityWorkflow = sw)', EntityWorkflowStep::class),
|
||||
// or the workflow which have only the initial step, with no transition
|
||||
'1 = (SELECT COUNT(ews.id) FROM chill_main_entity_workflow_step ews WHERE ews.step = :initial AND ews.transitionAt IS NULL AND ews.createdAt < :olderThanDate AND ews.entityWorkflow = sw.id)',
|
||||
sprintf('1 = (SELECT COUNT(ews3.id) FROM %s ews3 WHERE ews3.currentStep = :initial AND ews3.transitionAt IS NULL AND ews3.entityWorkflow = sw)', EntityWorkflowStep::class),
|
||||
)
|
||||
)
|
||||
->andWhere('sw.createdAt < :olderThanDate')
|
||||
->setParameter('olderThanDate', $olderThanDate);
|
||||
->setParameter('olderThanDate', $olderThanDate)
|
||||
->setParameter('initial', 'initial')
|
||||
;
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
return $qb->getQuery()->toIterable();
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
@@ -261,6 +265,7 @@ class EntityWorkflowRepository implements ObjectRepository
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':user', 'step.destUser'),
|
||||
$qb->expr()->isMemberOf(':user', 'step.destUserByAccessKey'),
|
||||
$qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF step.destUserGroups AND :user MEMBER OF ug.users', UserGroup::class))
|
||||
),
|
||||
$qb->expr()->isNull('step.transitionAfter'),
|
||||
$qb->expr()->eq('step.isFinal', "'FALSE'")
|
||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
@@ -65,7 +66,11 @@ readonly class EntityWorkflowStepRepository implements ObjectRepository
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isMemberOf(':user', 'e.destUser'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isMemberOf(':user', 'e.destUser'),
|
||||
$qb->expr()->isMemberOf(':user', 'e.destUserByAccessKey'),
|
||||
$qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF e.destUserGroups AND :user MEMBER OF ug.users', UserGroup::class))
|
||||
),
|
||||
$qb->expr()->isNull('e.transitionAt'),
|
||||
$qb->expr()->eq('e.isFinal', ':bool'),
|
||||
)
|
||||
|
@@ -233,7 +233,7 @@ div.wrap-header {
|
||||
}
|
||||
&:last-child {}
|
||||
|
||||
div.wh-col {
|
||||
& > div.wh-col {
|
||||
&:first-child {
|
||||
flex-grow: 0; flex-shrink: 1; flex-basis: auto;
|
||||
}
|
||||
|
@@ -1,12 +0,0 @@
|
||||
const buildLinkCreate = function(workflowName, relatedEntityClass, relatedEntityId) {
|
||||
let params = new URLSearchParams();
|
||||
params.set('entityClass', relatedEntityClass);
|
||||
params.set('entityId', relatedEntityId);
|
||||
params.set('workflow', workflowName);
|
||||
|
||||
return `/fr/main/workflow/create?`+params.toString();
|
||||
};
|
||||
|
||||
export {
|
||||
buildLinkCreate,
|
||||
};
|
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Constructs a URL for creating a workflow link associated with a specific entity.
|
||||
*
|
||||
* @param {string} workflowName - The name of the workflow to associate.
|
||||
* @param {string} relatedEntityClass - The class/type of the related entity.
|
||||
* @param {number|undefined} relatedEntityId - The unique identifier of the related entity. Must be defined.
|
||||
*
|
||||
* @returns {string} The constructed URL containing query parameters based on the provided arguments.
|
||||
*
|
||||
* @throws {Error} If the related entity ID is undefined.
|
||||
*/
|
||||
export const buildLinkCreate = (workflowName: string, relatedEntityClass: string, relatedEntityId: number|undefined): string => {
|
||||
if (typeof (relatedEntityId) === 'undefined') {
|
||||
throw new Error("the related entity id is not set");
|
||||
}
|
||||
let params = new URLSearchParams();
|
||||
params.set('entityClass', relatedEntityClass);
|
||||
params.set('entityId', relatedEntityId.toString(10));
|
||||
params.set('workflow', workflowName);
|
||||
|
||||
return `/fr/main/workflow/create?`+params.toString();
|
||||
};
|
@@ -30,6 +30,12 @@
|
||||
*/
|
||||
import './collection.scss';
|
||||
|
||||
declare global {
|
||||
interface GlobalEventHandlersEventMap {
|
||||
'show-hide-show': CustomEvent<{id: number, froms: HTMLElement[], container: HTMLElement}>,
|
||||
}
|
||||
}
|
||||
|
||||
export class CollectionEventPayload {
|
||||
collection: HTMLUListElement;
|
||||
entry: HTMLLIElement;
|
||||
@@ -107,20 +113,34 @@ export const buildRemoveButton = (collection: HTMLUListElement, entry: HTMLLIEle
|
||||
return button;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const collectionsInit = new Set<string>;
|
||||
const buttonsInit = new Set<string>();
|
||||
|
||||
const initialize = function (target: Document|Element): void {
|
||||
let
|
||||
addButtons: NodeListOf<HTMLButtonElement> = document.querySelectorAll("button[data-collection-add-target]"),
|
||||
collections: NodeListOf<HTMLUListElement> = document.querySelectorAll("ul[data-collection-regular]");
|
||||
|
||||
for (let i = 0; i < addButtons.length; i++) {
|
||||
let addButton = addButtons[i];
|
||||
const addButton = addButtons[i];
|
||||
const uniqid = addButton.dataset.uniqid as string;
|
||||
if (buttonsInit.has(uniqid)) {
|
||||
continue;
|
||||
}
|
||||
buttonsInit.add(uniqid);
|
||||
addButton.addEventListener('click', (e: Event) => {
|
||||
e.preventDefault();
|
||||
handleAdd(e.target);
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < collections.length; i++) {
|
||||
let entries: NodeListOf<HTMLLIElement> = collections[i].querySelectorAll(':scope > li');
|
||||
const collection = collections[i];
|
||||
const uniqid = collection.dataset.uniqid as string;
|
||||
if (collectionsInit.has(uniqid)) {
|
||||
continue;
|
||||
}
|
||||
collectionsInit.add(uniqid);
|
||||
let entries: NodeListOf<HTMLLIElement> = collection.querySelectorAll(':scope > li');
|
||||
for (let j = 0; j < entries.length; j++) {
|
||||
if (entries[j].dataset.collectionEmptyExplain === "1") {
|
||||
continue;
|
||||
@@ -128,4 +148,13 @@ window.addEventListener('load', () => {
|
||||
initializeRemove(collections[i], entries[j]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
initialize(document);
|
||||
});
|
||||
|
||||
window.addEventListener('show-hide-show', (event: CustomEvent<{id: number; container: HTMLElement; froms: HTMLElement[]}>) => {
|
||||
const container = event.detail.container as HTMLElement;
|
||||
initialize(container);
|
||||
})
|
||||
|
@@ -44,7 +44,9 @@ function loadDynamicPicker(element) {
|
||||
':suggested="notPickedSuggested" ' +
|
||||
':label="label" ' +
|
||||
'@addNewEntity="addNewEntity" ' +
|
||||
'@removeEntity="removeEntity"></pick-entity>',
|
||||
'@removeEntity="removeEntity" ' +
|
||||
'@addNewEntityProcessEnded="addNewEntityProcessEnded"' +
|
||||
'></pick-entity>',
|
||||
components: {
|
||||
PickEntity,
|
||||
},
|
||||
@@ -97,10 +99,11 @@ function loadDynamicPicker(element) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.submit_on_adding_new_entity) {
|
||||
input.form.submit();
|
||||
}
|
||||
},
|
||||
addNewEntityProcessEnded() {
|
||||
if (this.submit_on_adding_new_entity) {
|
||||
input.form.submit();
|
||||
}
|
||||
},
|
||||
removeEntity({entity}) {
|
||||
if (-1 === this.suggested.findIndex(e => e.type === entity.type && e.id === entity.id)) {
|
||||
|
@@ -1,27 +1,20 @@
|
||||
import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js';
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
let
|
||||
const
|
||||
divTransitions = document.querySelector('#transitions'),
|
||||
futureDestUsersContainer = document.querySelector('#futureDests')
|
||||
personSignatureField = document.querySelector('#person-signature-field');
|
||||
userSignatureField = document.querySelector('#user-signature-field');
|
||||
signatureTypeChoices = document.querySelector('#signature-type-choice');
|
||||
personChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_0');
|
||||
userChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_1');
|
||||
signatureZone = document.querySelector('#signature-zone');
|
||||
;
|
||||
|
||||
let
|
||||
transitionFilterContainer = document.querySelector('#transitionFilter'),
|
||||
transitionsContainer = document.querySelector('#transitions')
|
||||
futureDestUsersContainer = document.querySelector('#futureDests'),
|
||||
personSignatureField = document.querySelector('#person-signature-field'),
|
||||
userSignatureField = document.querySelector('#user-signature-field'),
|
||||
signatureTypeChoices = document.querySelector('#signature-type-choice'),
|
||||
personChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_0'),
|
||||
userChoice = document.querySelector('#workflow_step_isPersonOrUserSignature_1'),
|
||||
signatureZone = document.querySelector('#signature-zone'),
|
||||
transitionFilterContainer = document.querySelector('#transitionFilter'),
|
||||
transitionsContainer = document.querySelector('#transitions'),
|
||||
sendExternalContainer = document.querySelector('#sendExternalContainer')
|
||||
;
|
||||
|
||||
// ShowHide instance for signatureTypeChoices. This should always be present in the DOM and we toggle visibility.
|
||||
// The field is not mapped and so not submitted with the form. Without it's presence upon DOM loading other show hides do not function well.
|
||||
signatureTypeChoices.style.display = 'none';
|
||||
|
||||
|
||||
// ShowHide instance for future dest users
|
||||
new ShowHide({
|
||||
debug: false,
|
||||
@@ -32,15 +25,17 @@ window.addEventListener('DOMContentLoaded', function() {
|
||||
for (let transition of froms) {
|
||||
for (let input of transition.querySelectorAll('input')) {
|
||||
if (input.checked) {
|
||||
if ('1' === input.dataset.isSentExternal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const inputData = JSON.parse(input.getAttribute('data-is-signature'))
|
||||
if (inputData.includes('person') || inputData.includes('user')) {
|
||||
signatureTypeChoices.style.display = '';
|
||||
return false;
|
||||
} else {
|
||||
personChoice.checked = false
|
||||
userChoice.checked = false
|
||||
|
||||
signatureTypeChoices.style.display = 'none';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +45,26 @@ window.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// ShowHide instance for send external
|
||||
new ShowHide({
|
||||
debug: false,
|
||||
load_event: null,
|
||||
froms: [divTransitions],
|
||||
container: [sendExternalContainer],
|
||||
test: function(froms, event) {
|
||||
for (let transition of froms) {
|
||||
for (let input of transition.querySelectorAll('input')) {
|
||||
if (input.checked) {
|
||||
if ('1' === input.dataset.isSentExternal) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
// ShowHide signature zone
|
||||
new ShowHide({
|
||||
debug: false,
|
||||
@@ -62,7 +77,6 @@ window.addEventListener('DOMContentLoaded', function() {
|
||||
if (input.checked) {
|
||||
const inputData = JSON.parse(input.getAttribute('data-is-signature'))
|
||||
if (inputData.includes('person') || inputData.includes('user')) {
|
||||
signatureTypeChoices.style.display = '';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -1,164 +1,181 @@
|
||||
export interface DateTime {
|
||||
datetime: string;
|
||||
datetime8601: string
|
||||
datetime: string;
|
||||
datetime8601: string;
|
||||
}
|
||||
|
||||
export interface Civility {
|
||||
id: number;
|
||||
// TODO
|
||||
id: number;
|
||||
// TODO
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
id: number;
|
||||
type: "user_job";
|
||||
label: {
|
||||
"fr": string; // could have other key. How to do that in ts ?
|
||||
}
|
||||
id: number;
|
||||
type: "user_job";
|
||||
label: {
|
||||
fr: string; // could have other key. How to do that in ts ?
|
||||
};
|
||||
}
|
||||
|
||||
export interface Center {
|
||||
id: number;
|
||||
type: "center";
|
||||
name: string;
|
||||
id: number;
|
||||
type: "center";
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Scope {
|
||||
id: number;
|
||||
type: "scope";
|
||||
name: {
|
||||
"fr": string
|
||||
}
|
||||
id: number;
|
||||
type: "scope";
|
||||
name: {
|
||||
fr: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResultItem<T> {
|
||||
result: T;
|
||||
relevance: number;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
type: "user";
|
||||
id: number;
|
||||
username: string;
|
||||
text: string;
|
||||
text_without_absence: string;
|
||||
email: string;
|
||||
user_job: Job;
|
||||
label: string;
|
||||
// todo: mainCenter; mainJob; etc..
|
||||
type: "user";
|
||||
id: number;
|
||||
username: string;
|
||||
text: string;
|
||||
text_without_absence: string;
|
||||
email: string;
|
||||
user_job: Job;
|
||||
label: string;
|
||||
// todo: mainCenter; mainJob; etc..
|
||||
}
|
||||
|
||||
export interface UserGroup {
|
||||
type: "user_group";
|
||||
id: number;
|
||||
label: TranslatableString;
|
||||
backgroundColor: string;
|
||||
foregroundColor: string;
|
||||
excludeKey: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type UserGroupOrUser = User | UserGroup;
|
||||
|
||||
export interface UserAssociatedInterface {
|
||||
type: "user";
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type TranslatableString = {
|
||||
fr?: string;
|
||||
nl?: string;
|
||||
type: "user";
|
||||
id: number;
|
||||
}
|
||||
|
||||
export type TranslatableString = {
|
||||
fr?: string;
|
||||
nl?: string;
|
||||
};
|
||||
|
||||
export interface Postcode {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
center: Point;
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
center: Point;
|
||||
}
|
||||
|
||||
export type Point = {
|
||||
type: "Point";
|
||||
coordinates: [lat: number, lon: number];
|
||||
}
|
||||
type: "Point";
|
||||
coordinates: [lat: number, lon: number];
|
||||
};
|
||||
|
||||
export interface Country {
|
||||
id: number;
|
||||
name: TranslatableString;
|
||||
code: string;
|
||||
id: number;
|
||||
name: TranslatableString;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export type AddressRefStatus = 'match'|'to_review'|'reviewed';
|
||||
export type AddressRefStatus = "match" | "to_review" | "reviewed";
|
||||
|
||||
export interface Address {
|
||||
type: "address";
|
||||
address_id: number;
|
||||
text: string;
|
||||
street: string;
|
||||
streetNumber: string;
|
||||
postcode: Postcode;
|
||||
country: Country;
|
||||
floor: string | null;
|
||||
corridor: string | null;
|
||||
steps: string | null;
|
||||
flat: string | null;
|
||||
buildingName: string | null;
|
||||
distribution: string | null;
|
||||
extra: string | null;
|
||||
confidential: boolean;
|
||||
lines: string[];
|
||||
addressReference: AddressReference | null;
|
||||
validFrom: DateTime;
|
||||
validTo: DateTime | null;
|
||||
point: Point | null;
|
||||
refStatus: AddressRefStatus;
|
||||
isNoAddress: boolean;
|
||||
type: "address";
|
||||
address_id: number;
|
||||
text: string;
|
||||
street: string;
|
||||
streetNumber: string;
|
||||
postcode: Postcode;
|
||||
country: Country;
|
||||
floor: string | null;
|
||||
corridor: string | null;
|
||||
steps: string | null;
|
||||
flat: string | null;
|
||||
buildingName: string | null;
|
||||
distribution: string | null;
|
||||
extra: string | null;
|
||||
confidential: boolean;
|
||||
lines: string[];
|
||||
addressReference: AddressReference | null;
|
||||
validFrom: DateTime;
|
||||
validTo: DateTime | null;
|
||||
point: Point | null;
|
||||
refStatus: AddressRefStatus;
|
||||
isNoAddress: boolean;
|
||||
}
|
||||
|
||||
export interface AddressWithPoint extends Address {
|
||||
point: Point
|
||||
point: Point;
|
||||
}
|
||||
|
||||
export interface AddressReference {
|
||||
id: number;
|
||||
createdAt: DateTime | null;
|
||||
deletedAt: DateTime | null;
|
||||
municipalityCode: string;
|
||||
point: Point;
|
||||
postcode: Postcode;
|
||||
refId: string;
|
||||
source: string;
|
||||
street: string;
|
||||
streetNumber: string;
|
||||
updatedAt: DateTime | null;
|
||||
id: number;
|
||||
createdAt: DateTime | null;
|
||||
deletedAt: DateTime | null;
|
||||
municipalityCode: string;
|
||||
point: Point;
|
||||
postcode: Postcode;
|
||||
refId: string;
|
||||
source: string;
|
||||
street: string;
|
||||
streetNumber: string;
|
||||
updatedAt: DateTime | null;
|
||||
}
|
||||
|
||||
export interface SimpleGeographicalUnit {
|
||||
id: number;
|
||||
layerId: number;
|
||||
unitName: string;
|
||||
unitRefId: string;
|
||||
id: number;
|
||||
layerId: number;
|
||||
unitName: string;
|
||||
unitRefId: string;
|
||||
}
|
||||
|
||||
export interface GeographicalUnitLayer {
|
||||
id: number;
|
||||
name: TranslatableString;
|
||||
refId: string;
|
||||
id: number;
|
||||
name: TranslatableString;
|
||||
refId: string;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
type: "location";
|
||||
id: number;
|
||||
active: boolean;
|
||||
address: Address | null;
|
||||
availableForUsers: boolean;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
updatedAt: DateTime | null;
|
||||
updatedBy: User | null;
|
||||
email: string | null
|
||||
name: string;
|
||||
phonenumber1: string | null;
|
||||
phonenumber2: string | null;
|
||||
locationType: LocationType;
|
||||
type: "location";
|
||||
id: number;
|
||||
active: boolean;
|
||||
address: Address | null;
|
||||
availableForUsers: boolean;
|
||||
createdAt: DateTime | null;
|
||||
createdBy: User | null;
|
||||
updatedAt: DateTime | null;
|
||||
updatedBy: User | null;
|
||||
email: string | null;
|
||||
name: string;
|
||||
phonenumber1: string | null;
|
||||
phonenumber2: string | null;
|
||||
locationType: LocationType;
|
||||
}
|
||||
|
||||
export interface LocationAssociated {
|
||||
type: "location";
|
||||
id: number;
|
||||
type: "location";
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface LocationType {
|
||||
type: "location-type";
|
||||
id: number;
|
||||
active: boolean;
|
||||
addressRequired: "optional" | "required";
|
||||
availableForUsers: boolean;
|
||||
editableByUsers: boolean;
|
||||
contactData: "optional" | "required";
|
||||
title: TranslatableString;
|
||||
type: "location-type";
|
||||
id: number;
|
||||
active: boolean;
|
||||
addressRequired: "optional" | "required";
|
||||
availableForUsers: boolean;
|
||||
editableByUsers: boolean;
|
||||
contactData: "optional" | "required";
|
||||
title: TranslatableString;
|
||||
}
|
||||
|
||||
export interface NewsItemType {
|
||||
@@ -168,3 +185,8 @@ export interface NewsItemType {
|
||||
startDate: DateTime;
|
||||
endDate: DateTime | null;
|
||||
}
|
||||
|
||||
export interface WorkflowAvailable {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
@@ -62,7 +62,7 @@ export default {
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
emits: ['addNewEntity', 'removeEntity'],
|
||||
emits: ['addNewEntity', 'removeEntity', 'addNewEntityProcessEnded'],
|
||||
components: {
|
||||
AddPersons,
|
||||
},
|
||||
@@ -121,6 +121,7 @@ export default {
|
||||
);
|
||||
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||
modal.showModal = false;
|
||||
this.$emit('addNewEntityProcessEnded');
|
||||
},
|
||||
removeEntity(entity) {
|
||||
if (!this.$props.removableIfSet) {
|
||||
|
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import {UserGroup} from "../../../types";
|
||||
import {computed} from "vue";
|
||||
|
||||
interface UserGroupRenderBoxProps {
|
||||
userGroup: UserGroup;
|
||||
}
|
||||
|
||||
const props = defineProps<UserGroupRenderBoxProps>();
|
||||
|
||||
const styles = computed<{color: string, "background-color": string}>(() => {
|
||||
return {
|
||||
color: props.userGroup.foregroundColor,
|
||||
"background-color": props.userGroup.backgroundColor,
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="badge-user-group" :style="styles">{{ userGroup.label.fr }}</span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -102,11 +102,17 @@ export default {
|
||||
},
|
||||
getPopContent(step) {
|
||||
if (step.transitionPrevious != null) {
|
||||
return `<ul class="small_in_title">
|
||||
<li><span class="item-key">${i18n.messages.fr.by} : </span><b>${step.transitionPreviousBy.text}</b></li>
|
||||
<li><span class="item-key">${i18n.messages.fr.at} : </span><b>${this.formatDate(step.transitionPreviousAt.datetime)}</b></li>
|
||||
</ul>`
|
||||
;
|
||||
if (step.transitionPreviousBy !== null) {
|
||||
return `<ul class="small_in_title">
|
||||
<li><span class="item-key">${i18n.messages.fr.by} : </span><b>${step.transitionPreviousBy.text}</b></li>
|
||||
<li><span class="item-key">${i18n.messages.fr.at} : </span><b>${this.formatDate(step.transitionPreviousAt.datetime)}</b></li>
|
||||
</ul>`
|
||||
;
|
||||
} else {
|
||||
return `<ul class="small_in_title">
|
||||
<li><span class="item-key">${i18n.messages.fr.at} : </span><b>${this.formatDate(step.transitionPreviousAt.datetime)}</b></li>
|
||||
</ul>`
|
||||
}
|
||||
}
|
||||
},
|
||||
formatDate(datetime) {
|
||||
|
@@ -1,63 +1,66 @@
|
||||
<template>
|
||||
<template v-if="workflowsAvailables.length >= 1">
|
||||
<template v-if="props.workflowsAvailables.length >= 1">
|
||||
<div class="dropdown d-grid gap-2">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Créer un workflow
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="createWorkflowButton">
|
||||
<li v-for="w in workflowsAvailables" :key="w.name">
|
||||
<a class="dropdown-item" :href="makeLink(w.name)" @click.prevent="goToGenerateWorkflow($event, w.name)">{{ w.text }}</a>
|
||||
<li v-for="w in props.workflowsAvailables" :key="w.name">
|
||||
<button class="dropdown-item" type="button" @click.prevent="goToGenerateWorkflow($event, w.name)">{{ w.text }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
|
||||
import {buildLinkCreate} from '../../../lib/entity-workflow/api';
|
||||
import {WorkflowAvailable} from "../../../types";
|
||||
|
||||
export default {
|
||||
name: "PickWorkflow",
|
||||
props: {
|
||||
relatedEntityClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
relatedEntityId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
workflowsAvailables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
preventDefaultMoveToGenerate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
goToGenerateWorkflowPayload: {
|
||||
required: false,
|
||||
default: {}
|
||||
},
|
||||
},
|
||||
emits: ['goToGenerateWorkflow'],
|
||||
methods: {
|
||||
makeLink(workflowName) {
|
||||
return buildLinkCreate(workflowName, this.relatedEntityClass, this.relatedEntityId);
|
||||
},
|
||||
goToGenerateWorkflow(event, workflowName) {
|
||||
console.log('goToGenerateWorkflow', event, workflowName);
|
||||
interface PickWorkflowConfig {
|
||||
relatedEntityClass: string;
|
||||
/**
|
||||
* Represents the identifier of a related entity.
|
||||
* This variable can store either a numerical value representing the entity's ID or an undefined value
|
||||
* if the related entity is not specified or available, for instance when the entity is created within
|
||||
* the interface and the id will be available later
|
||||
*/
|
||||
relatedEntityId: number|undefined;
|
||||
workflowsAvailables: WorkflowAvailable[];
|
||||
preventDefaultMoveToGenerate: boolean;
|
||||
goToGenerateWorkflowPayload: object;
|
||||
}
|
||||
|
||||
if (!this.$props.preventDefaultMoveToGenerate) {
|
||||
console.log('to go generate');
|
||||
window.location.assign(this.makeLink(workflowName));
|
||||
}
|
||||
const props = withDefaults(defineProps<PickWorkflowConfig>(), {preventDefaultMoveToGenerate: false, goToGenerateWorkflowPayload: {}});
|
||||
|
||||
this.$emit('goToGenerateWorkflow', {event, workflowName, link: this.makeLink(workflowName), payload: this.goToGenerateWorkflowPayload});
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(e: 'goToGenerateWorkflow', {event: MouseEvent, workflowName: string, isLinkValid: boolean, link: string, payload: object}): void;
|
||||
}>();
|
||||
|
||||
const makeLink = (workflowName: string): string => buildLinkCreate(workflowName, props.relatedEntityClass, props.relatedEntityId);
|
||||
|
||||
const goToGenerateWorkflow = (event: MouseEvent, workflowName: string): void => {
|
||||
console.log('goToGenerateWorkflow', event, workflowName);
|
||||
let link = '';
|
||||
let isLinkValid = false;
|
||||
|
||||
try {
|
||||
link = makeLink(workflowName);
|
||||
isLinkValid = true;
|
||||
} catch (e) {
|
||||
console.info("could not generate link to create workflow, maybe the relatedEntityId is not yet known", e);
|
||||
}
|
||||
|
||||
if (!props.preventDefaultMoveToGenerate) {
|
||||
window.location.assign(link);
|
||||
}
|
||||
|
||||
emit('goToGenerateWorkflow', {event, workflowName, link, isLinkValid, payload: props.goToGenerateWorkflowPayload});
|
||||
}
|
||||
|
||||
const goToDuplicateRelatedEntity = (event: MouseEvent, workflowName: string): void => {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@@ -0,0 +1 @@
|
||||
<span class="badge-user-group" style="color: {{ user_group.foregroundColor }}; background-color: {{ user_group.backgroundColor }};">{{ user_group.label|localize_translatable_string }}</span>
|
@@ -163,6 +163,7 @@
|
||||
<div class="chill-collection">
|
||||
<ul class="list-entry"
|
||||
{{ form.vars.js_caller }}="{{ form.vars.js_caller }}"
|
||||
data-uniqid="{{ form.vars.uniqid|escape('html_attr') }}"
|
||||
data-collection-name="{{ form.vars.name|escape('html_attr') }}"
|
||||
data-collection-identifier="{{ form.vars.identifier|escape('html_attr') }}"
|
||||
data-collection-button-remove-label="{{ form.vars.button_remove_label|trans|e }}"
|
||||
@@ -184,6 +185,8 @@
|
||||
|
||||
{% if form.vars.allow_add == 1 %}
|
||||
<button class="add-entry btn btn-misc"
|
||||
type="button"
|
||||
data-uniqid="{{ form.vars.uniqid|escape('html_attr') }}"
|
||||
data-collection-add-target="{{ form.vars.name|escape('html_attr') }}"
|
||||
data-form-prototype="{{ ('<div>' ~ form_widget(form.vars.prototype) ~ '</div>')|escape('html_attr') }}" >
|
||||
<i class="fa fa-plus fa-fw"></i>
|
||||
|
@@ -60,7 +60,7 @@
|
||||
data-bs-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<a href="#" class="more">{{ app.request.locale | capitalize }}</a>
|
||||
{{ app.request.locale | capitalize }}
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end"
|
||||
aria-labelledby="menu-languages">
|
||||
|
@@ -0,0 +1,21 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% include('@ChillMain/CRUD/_edit_title.html.twig') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
|
||||
{% block content_form_actions_save_and_show %}{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock admin_content %}
|
@@ -0,0 +1,86 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||
|
||||
{% block table_entities %}
|
||||
<div class="flex-table">
|
||||
{% for entity in entities %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<div class="wrap-header">
|
||||
<div class="wh-row">
|
||||
<div class="wh-col">
|
||||
{{ entity|chill_entity_render_box }}
|
||||
</div>
|
||||
<div class="wh-col">
|
||||
{%- if not entity.active -%}
|
||||
<div>
|
||||
<span class="badge bg-danger">{{ 'user_group.inactive'|trans }}</span>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
<div>{{ 'user_group.with_count_users'|trans({'count': entity.users|length}) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<strong>{{ 'user_group.with_users'|trans }}</strong>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for user in entity.userListByLabelAscending %}
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="wl-item chill-no-data-statement">{{ 'user_group.no_users'|trans }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<strong>{{ 'user_group.adminUsers'|trans }}</strong>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% for user in entity.adminUserListByLabelAscending %}
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="wl-item chill-no-data-statement">{{ 'user_group.no_admin_users'|trans }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions slim">
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_crud_admin_user_group_edit', {'id': entity.id}) }}" class="btn btn-edit"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block actions_before %}
|
||||
<li class='cancel'>
|
||||
<a href="{{ path('chill_main_admin_central') }}" class="btn btn-cancel">{{'Back to the admin'|trans }}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% endembed %}
|
||||
{% endblock %}
|
@@ -0,0 +1,160 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
|
||||
<style type="text/css">
|
||||
form.remove {
|
||||
display: inline-block;
|
||||
padding: 1px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
form:hover {
|
||||
animation-duration: 0.5s;
|
||||
animation-name: onHover;
|
||||
animation-iteration-count: 1;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
form.remove button.remove {
|
||||
display: inline;
|
||||
background-color: unset;
|
||||
border: none;
|
||||
color: var(--bs-chill-red);
|
||||
}
|
||||
|
||||
@keyframes onHover {
|
||||
from {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
to {
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title 'user_group.my_groups'|trans %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
{% if paginator.totalItems == 0 %}
|
||||
<p>{{ 'user_group.no_user_groups'|trans }}</p>
|
||||
{% else %}
|
||||
<div class="flex-table">
|
||||
{% for entity in groups %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<div class="wrap-header">
|
||||
<div class="wh-row">
|
||||
<div class="wh-col">
|
||||
{{ entity|chill_entity_render_box }}
|
||||
</div>
|
||||
<div class="wh-col">
|
||||
{%- if not entity.active -%}
|
||||
<div>
|
||||
<span class="badge bg-danger">{{ 'user_group.inactive'|trans }}</span>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
<div>{{ 'user_group.with_count_users'|trans({'count': entity.users|length}) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<strong>{{ 'user_group.with_users'|trans }}</strong>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% if entity.users.contains(app.user) %}
|
||||
{% if is_granted('CHILL_MAIN_USER_GROUP_APPEND_TO_GROUP', entity) %}
|
||||
<form class="remove" method="POST" action="{{ chill_path_add_return_path('chill_main_user_groups_remove_user', {'id': entity.id, 'userId': app.user.id}) }}">
|
||||
<p class="wl-item">
|
||||
{{ 'user_group.me'|trans }}
|
||||
<button class="remove" type="submit"><i class="fa fa-times"></i></button>
|
||||
</p>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="wl-item">
|
||||
{% if entity.users|length > 1 %}{{ 'user_group.me_and'|trans }}{% else %}{{ 'user_group.me_only'|trans }}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for user in entity.userListByLabelAscending %}
|
||||
{% if user is not same as app.user %}
|
||||
{% if is_granted('CHILL_MAIN_USER_GROUP_APPEND_TO_GROUP', entity) %}
|
||||
<form class="remove" method="POST" action="{{ chill_path_add_return_path('chill_main_user_groups_remove_user', {'id': entity.id, 'userId': user.id}) }}">
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
<button class="remove" type="submit"><i class="fa fa-times"></i></button>
|
||||
</p>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if entity.adminUsers|length > 0 %}
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<strong>{{ 'user_group.adminUsers'|trans }}</strong>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
{% if entity.adminUsers.contains(app.user) %}
|
||||
<p class="wl-item">{% if entity.adminUsers|length > 1 %}{{ 'user_group.me_and'|trans }}{% else %}{{ 'user_group.me_only'|trans }}{% endif %}</p>
|
||||
{% endif %}
|
||||
{% for user in entity.adminUserListByLabelAscending %}
|
||||
{% if user is not same as app.user %}
|
||||
<p class="wl-item">
|
||||
<span class="badge-user">
|
||||
{{ user|chill_entity_render_box }}
|
||||
</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif -%}
|
||||
{%- set form = forms.offsetGet(entity) %}
|
||||
{%- if form is not null -%}
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions slim">
|
||||
<li>
|
||||
{{- form_start(form) -}}
|
||||
{{- form_widget(form.users) -}}
|
||||
{{- form_end(form) -}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{{ chill_pagination(paginator) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -0,0 +1,21 @@
|
||||
{% extends '@ChillMain/CRUD/Admin/index.html.twig' %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
|
||||
{% block content_form_actions_save_and_show %}{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock admin_content %}
|
@@ -6,7 +6,7 @@
|
||||
{{ form_errors(transition_form) }}
|
||||
|
||||
{% set step = entity_workflow.currentStepChained %}
|
||||
{% set labels = workflow_metadata(entity_workflow, 'label', step.currentStep) %}
|
||||
{% set labels = workflow_metadata(entity_workflow, 'label', step.currentStep, entity_workflow.workflowName) %}
|
||||
{% set label = labels is null ? step.currentStep : labels|localize_translatable_string %}
|
||||
|
||||
<div class="card">
|
||||
@@ -82,17 +82,33 @@
|
||||
{{ form_row(transition_form.futureCcUsers) }}
|
||||
{{ form_errors(transition_form.futureCcUsers) }}
|
||||
</div>
|
||||
<div id="future-dest-emails">
|
||||
{{ form_row(transition_form.futureDestEmails) }}
|
||||
{{ form_errors(transition_form.futureDestEmails) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sendExternalContainer">
|
||||
{{ form_row(transition_form.futureDestineeThirdParties) }}
|
||||
{{ form_errors(transition_form.futureDestineeThirdParties) }}
|
||||
{{ form_row(transition_form.futureDestineeEmails) }}
|
||||
{{ form_errors(transition_form.futureDestineeEmails) }}
|
||||
</div>
|
||||
|
||||
<p>{{ form_label(transition_form.comment) }}</p>
|
||||
|
||||
{{ form_widget(transition_form.comment) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
{% if entity_workflow.isOnHoldByUser(app.user) %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ path('chill_main_workflow_remove_hold', {'id': entity_workflow.currentStep.id }) }}"><i class="fa fa-hourglass"></i>
|
||||
{{ 'workflow.Remove hold'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ path('chill_main_workflow_on_hold', {'id': entity_workflow.id}) }}"><i class="fa fa-hourglass"></i>
|
||||
{{ 'workflow.Put on hold'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||
</li>
|
||||
@@ -115,15 +131,6 @@
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if entity_workflow.currentStep.destEmail|length > 0 %}
|
||||
<p><b>{{ 'workflow.An access key was also sent to those addresses'|trans }} :</b></p>
|
||||
<ul>
|
||||
{% for e in entity_workflow.currentStep.destEmail -%}
|
||||
<li><a href="mailto:{{ e|escape('html_attr') }}">{{ e }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
|
||||
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
||||
<ul>
|
||||
|
@@ -1,8 +1,5 @@
|
||||
{% set acl = "0" %}
|
||||
{% if is_granted('CHILL_MAIN_WORKFLOW_CREATE', blank_workflow) %}
|
||||
{% set acl = "1" %}
|
||||
{% endif %}
|
||||
|
||||
{% set acl = "1" %}
|
||||
{# note: the list of available workflows already check that it is possible to create a given workflow #}
|
||||
{# vue component #}
|
||||
<div data-list-workflows="1"
|
||||
data-workflows="{{ entity_workflows_json|json_encode|e('html_attr') }}"
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="flex-table">
|
||||
{% for step in entity_workflow.stepsChained %}
|
||||
{% set place_labels = workflow_metadata(entity_workflow, 'label', step.currentStep) %}
|
||||
{% set place_labels = workflow_metadata(entity_workflow, 'label', step.currentStep, entity_workflow.workflowName) %}
|
||||
{% set place_label = place_labels is null ? step.currentStep : place_labels|localize_translatable_string %}
|
||||
|
||||
<div class="item-bloc {{ 'bloc' ~ step.id }} {% if loop.first %}initial{% endif %}">
|
||||
@@ -13,31 +13,31 @@
|
||||
{{ 'workflow.No transitions'|trans }}
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="item-col">
|
||||
{% if step.previous is not null and step.previous.freezeAfter == true %}
|
||||
<i class="fa fa-snowflake-o fa-sm me-1" title="{{ 'workflow.Freezed'|trans }}"></i>
|
||||
{% endif %}
|
||||
{% if loop.last %}
|
||||
{% if entity_workflow.isOnHoldAtCurrentStep %}
|
||||
{% for hold in step.holdsOnStep %}
|
||||
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold by'|trans({'by': hold.byUser|chill_entity_render_string})|escape('html_attr') }}">{{ 'workflow.On hold by'|trans({'by': hold.byUser|chill_entity_render_string}) }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="item-col flex-column align-items-end">
|
||||
<div class="decided">
|
||||
{{ place_label }}
|
||||
</div>
|
||||
{#
|
||||
<div class="decided">
|
||||
<i class="fa fa-times fa-fw text-danger"></i>
|
||||
Refusé
|
||||
</div>
|
||||
#}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% if step.next is not null %}
|
||||
{% set transition = chill_workflow_transition_by_string(step.entityWorkflow, step.transitionAfter) %}
|
||||
{% set transition_labels = workflow_metadata(step.entityWorkflow, 'label', transition) %}
|
||||
{% set transition_labels = workflow_metadata(step.entityWorkflow, 'label', transition, step.entityWorkflow.workflowName) %}
|
||||
{% set transition_label = transition_labels is null ? step.transitionAfter : transition_labels|localize_translatable_string %}
|
||||
{% set forward = workflow_metadata(step.entityWorkflow, 'isForward', transition) %}
|
||||
{% set forward = workflow_metadata(step.entityWorkflow, 'isForward', transition, step.entityWorkflow.workflowName) %}
|
||||
<div class="item-row separator">
|
||||
<div class="item-col" style="width: inherit;">
|
||||
<div>
|
||||
@@ -71,19 +71,33 @@
|
||||
</blockquote>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if loop.last and step.allDestUser|length > 0 %}
|
||||
{% if not loop.last and step.signatures|length > 0 %}
|
||||
<div class="separator">
|
||||
<div>
|
||||
<p><b>{{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p>
|
||||
<div>
|
||||
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if loop.last and not step.isFinal %}
|
||||
<div class="item-row separator">
|
||||
<div>
|
||||
{% if step.destUser|length > 0 %}
|
||||
{% if step.destUser|length > 0 or step.destUserGroups|length > 0 %}
|
||||
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }} : </b></p>
|
||||
<ul>
|
||||
{% for u in step.destUser %}
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}
|
||||
{% if entity_workflow.isOnHoldAtCurrentStep %}
|
||||
<li>
|
||||
<span class="badge-user">{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</span>
|
||||
{% if step.isOnHoldByUser(u) %}
|
||||
<span class="badge bg-success rounded-pill" title="{{ 'workflow.On hold'|trans|escape('html_attr') }}">{{ 'workflow.On hold'|trans }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for u in step.destUserGroups %}
|
||||
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
@@ -115,6 +129,17 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if step.signatures|length > 0 %}
|
||||
<p><b>{{ 'workflow.signature_required_title'|trans({'nb_signatures': step.signatures|length}) }} :</b></p>
|
||||
{{ include('@ChillMain/Workflow/_signature_list.html.twig', {'signatures': step.signatures, is_small: true }) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step.sends|length > 0 %}
|
||||
<div>
|
||||
<p><b>{{ 'workflow.sent_through_secured_link'|trans }}</b></p>
|
||||
|
||||
{{ include('@ChillMain/Workflow/_send_views_list.html.twig', {'sends': step.sends}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
@@ -0,0 +1,35 @@
|
||||
<div class="container">
|
||||
{% for send in sends %}
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-5">
|
||||
{% if send.destineeKind == 'thirdParty' %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'thirdParty', id: send.destineeThirdParty.id },
|
||||
buttonText: send.destineeThirdParty|chill_entity_render_string,
|
||||
} %}
|
||||
{% else %}
|
||||
<a href="mailto:{{ send.destineeEmail }}">{{ send.destineeEmail }}</a>
|
||||
{% endif %}
|
||||
{% if not send.expired %}
|
||||
<p><small>{{ 'workflow.send_external_message.document_available_until'|trans({'expiration': send.expireAt}) }}</small></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-7 text-end">
|
||||
<p><small>{{ 'workflow.external_views.number_of_views'|trans({'numberOfViews': send.views|length}) }}</small></p>
|
||||
{% if send.views|length > 0 %}
|
||||
<p><small>{{ 'workflow.external_views.last_view_at'|trans({'at': send.lastView.viewAt }) }}</small></p>
|
||||
<p>
|
||||
<small>
|
||||
<b>{{ 'workflow.public_views_by_ip'|trans }} :</b>
|
||||
{%- for ip, vs in send.viewsByIp -%}
|
||||
<span title="{% for v in vs %}{{ v.viewAt|format_datetime('short', 'short')|escape('html_attr') }}{% if not loop.last %}{{ ', '|escape('html_attr') }}{% endif %}{% endfor %}">{{ ip }} ({{ vs|length }}x)</span>
|
||||
{%- endfor -%}
|
||||
</small>
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
@@ -1,43 +1,3 @@
|
||||
<h2>{{ 'workflow.signature_zone.title'|trans }}</h2>
|
||||
|
||||
<div class="container">
|
||||
{% for s in signatures %}
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-8">
|
||||
{% if s.signerKind == 'person' %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'person', id: s.signer.id },
|
||||
buttonText: s.signer|chill_entity_render_string,
|
||||
isDead: s.signer.deathDate is not null
|
||||
} %}
|
||||
{% else %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'user', id: s.signer.id },
|
||||
buttonText: s.signer|chill_entity_render_string,
|
||||
} %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4">
|
||||
{% if s.isSigned %}
|
||||
<span class="text-end">{{ 'workflow.signature_zone.has_signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
|
||||
{% else %}
|
||||
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %}
|
||||
<ul class="record_actions slim">
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a>
|
||||
{% if s.state is same as('signed') %}
|
||||
<p class="updatedBy">{{ s.stateDate }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-end">{{ 'workflow.waiting_for_signature'|trans }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<h2>{{ 'workflow.signature_required_title'|trans({'nb_signatures': signatures|length}) }}</h2>
|
||||
|
||||
{{ include('@ChillMain/Workflow/_signature_list.html.twig') }}
|
||||
|
@@ -0,0 +1,52 @@
|
||||
<div class="container">
|
||||
{% for s in signatures %}
|
||||
<div class="row row-hover align-items-center">
|
||||
<div class="col-sm-12 col-md-5">
|
||||
{% if s.signerKind == 'person' %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'person', id: s.signer.id },
|
||||
buttonText: s.signer|chill_entity_render_string,
|
||||
isDead: s.signer.deathDate is not null
|
||||
} %}
|
||||
{% else %}
|
||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: 'user', id: s.signer.id },
|
||||
buttonText: s.signer|chill_entity_render_string,
|
||||
} %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-7 text-end">
|
||||
{% if s.isSigned %}
|
||||
<span class="text-end">{{ 'workflow.signature.signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
|
||||
{% elseif s.isCanceled %}
|
||||
<span class="text-end">{{ 'workflow.signature.canceled_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
|
||||
{% elseif s.isRejected%}
|
||||
<span class="text-end">{{ 'workflow.signature.rejected_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
|
||||
{% else %}
|
||||
{% if (is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) or is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s)) %}
|
||||
<ul class="record_actions slim {% if is_small|default(false) %}small{% endif %}">
|
||||
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_REJECT', s) %}
|
||||
<li>
|
||||
<a class="btn btn-remove" href="{{ chill_path_add_return_path('chill_main_workflow_signature_reject', { 'id': s.id}) }}">{{ 'workflow.signature_zone.button_reject'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_CANCEL', s) %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_cancel', { 'id': s.id}) }}">{{ 'workflow.signature_zone.button_cancel'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@@ -1,12 +1,11 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block title %}
|
||||
{% block title -%}
|
||||
{{ 'Workflow'|trans }}
|
||||
{% endblock %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_subscribe') }}
|
||||
{{ encore_entry_script_tags('page_workflow_show') }}
|
||||
@@ -62,26 +61,17 @@
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
||||
{% if signatures|length > 0 %}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_signature.html.twig' %}</section>
|
||||
{% elseif entity_workflow.currentStep.sends|length > 0 %}
|
||||
<section class="step my-4">
|
||||
<h2>{{ 'workflow.external_views.title'|trans({'numberOfSends': entity_workflow.currentStep.sends|length }) }}</h2>
|
||||
{% include '@ChillMain/Workflow/_send_views_list.html.twig' with {'sends': entity_workflow.currentStep.sends} %}
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>
|
||||
{% endif %}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>{#
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}
|
||||
{# <section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
{% if entity_workflow.isOnHoldByUser(app.user) %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ path('chill_main_workflow_remove_hold', {'id': entity_workflow.currentStep.id }) }}"><i class="fa fa-hourglass"></i>
|
||||
{{ 'workflow.Remove hold'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a class="btn btn-misc" href="{{ path('chill_main_workflow_on_hold', {'id': entity_workflow.id}) }}"><i class="fa fa-hourglass"></i>
|
||||
{{ 'workflow.Put on hold'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -44,7 +44,7 @@
|
||||
{% endif %}
|
||||
{% if step.previous is not null %}
|
||||
{% set transition = chill_workflow_transition_by_string(step.entityWorkflow, step.previous.transitionAfter) %}
|
||||
{% set labels = workflow_metadata(step.entityWorkflow, 'label', transition) %}
|
||||
{% set labels = workflow_metadata(step.entityWorkflow, 'label', transition, step.entityWorkflow.workflowName) %}
|
||||
{% set label = labels is null ? step.previous.transitionAfter : labels|localize_translatable_string %}
|
||||
{{ label }}
|
||||
{% endif %}
|
||||
@@ -53,7 +53,7 @@
|
||||
{% macro breadcrumb(_ctx) %}
|
||||
<div class="breadcrumb">
|
||||
{% for step in _ctx.entity_workflow.stepsChained %}
|
||||
{% set labels = workflow_metadata(_ctx.entity_workflow, 'label', step.currentStep) %}
|
||||
{% set labels = workflow_metadata(_ctx.entity_workflow, 'label', step.currentStep, _ctx.entity_workflow.workflowName) %}
|
||||
{% set label = labels is null ? step.currentStep : labels|localize_translatable_string %}
|
||||
{% set popTitle = _self.popoverTitle(step) %}
|
||||
{% set popContent = _self.popoverContent(step) %}
|
||||
|
@@ -1,12 +1,10 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block title %}
|
||||
{{ 'Signature'|trans }}
|
||||
{% endblock %}
|
||||
{% block title 'workflow.signature_zone.metadata.sign_by'|trans({ '%name%' : person|chill_entity_render_string}) %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-10 workflow">
|
||||
<h1 class="mb-5">{{ 'workflow.signature_zone.metadata.sign_by'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname}) }}</h1>
|
||||
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||
|
||||
{% if metadata_form is not null %}
|
||||
{{ form_start(metadata_form) }}
|
@@ -0,0 +1,9 @@
|
||||
Chers membres du groupe {{ user_group.label|localize_translatable_string }},
|
||||
|
||||
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ place.text }}
|
||||
|
||||
Vous pouvez visualiser le workflow sur cette page:
|
||||
|
||||
{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id, '_locale': 'fr'})) }}
|
||||
|
||||
Cordialement,
|
@@ -1,15 +0,0 @@
|
||||
Madame, Monsieur,
|
||||
|
||||
Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ workflow.text }}.
|
||||
|
||||
Titre du workflow: "{{ entityTitle }}".
|
||||
|
||||
Vous êtes invité·e à valider cette étape. Pour obtenir un accès, vous pouvez cliquer sur le lien suivant:
|
||||
|
||||
{{ absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, 'accessKey': entity_workflow.currentStep.accessKey, '_locale': fr})) }}
|
||||
|
||||
Dès que vous aurez cliqué une fois sur le lien, vous serez autorisé à valider cette étape.
|
||||
|
||||
Notez que vous devez disposer d'un compte utilisateur valide dans Chill.
|
||||
|
||||
Cordialement,
|
@@ -1 +0,0 @@
|
||||
Un suivi {{ workflow.text }} demande votre attention: {{ entityTitle }}
|
@@ -0,0 +1,88 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
{%- set previous = send.entityWorkflowStepChained.previous -%}
|
||||
{%- if previous.transitionBy is not null -%}
|
||||
{%- set sender = previous.transitionBy|chill_entity_render_string -%}
|
||||
{%- else -%}
|
||||
{%- set sender = 'workflow.send_external_message.sender_system_user'|trans -%}
|
||||
{%- endif -%}
|
||||
<script type="application/ld+json">
|
||||
{%- set data = {
|
||||
"@context": "http://schema.org",
|
||||
"@type": "EmailMessage",
|
||||
"potentialAction": {
|
||||
"@type": "ViewAction",
|
||||
"url": absolute_url(path('chill_main_workflow_send_view_public', {'uuid': send.uuid, 'verificationKey': send.privateToken})),
|
||||
"name": 'workflow.send_external_message.see_docs_action_name'|trans
|
||||
},
|
||||
"description": 'workflow.send_external_message.see_doc_action_description'|trans({'sender': sender}),
|
||||
} -%}
|
||||
{{- data|json_encode|raw -}}
|
||||
</script>
|
||||
<div
|
||||
style='background-color:#F5F5F5;color:#262626;font-family:"Helvetica Neue", "Arial Nova", "Nimbus Sans", Arial, sans-serif;font-size:16px;font-weight:400;letter-spacing:0.15008px;line-height:1.5;margin:0;padding:32px 0;min-height:100%;width:100%'
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
style="margin:0 auto;max-width:600px;background-color:#FFFFFF"
|
||||
role="presentation"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr style="width:100%">
|
||||
<td>
|
||||
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||
{{ 'workflow.send_external_message.greeting'|trans }},
|
||||
</div>
|
||||
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||
{{ 'workflow.send_external_message.explanation'|trans({'sender': sender}) }}
|
||||
</div>
|
||||
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||
{{ 'workflow.send_external_message.confidentiality'|trans }}
|
||||
</div>
|
||||
<div style="text-align:center;padding:16px 24px 16px 24px">
|
||||
<a
|
||||
href="{{ absolute_url(path('chill_main_workflow_send_view_public', {'uuid': send.uuid, 'verificationKey': send.privateToken})) }}"
|
||||
style="color:#FFFFFF;font-size:16px;font-weight:bold;background-color:#334d5c;border-radius:4px;display:inline-block;padding:12px 20px;text-decoration:none"
|
||||
target="_blank"
|
||||
><span
|
||||
><!--[if mso
|
||||
]><i
|
||||
style="letter-spacing: 20px;mso-font-width:-100%;mso-text-raise:30"
|
||||
hidden
|
||||
> </i
|
||||
><!
|
||||
[endif]--></span
|
||||
><span>
|
||||
{{ 'workflow.send_external_message.button_content'|trans({'sender': sender}) }}
|
||||
</span><span
|
||||
><!--[if mso
|
||||
]><i
|
||||
style="letter-spacing: 20px;mso-font-width:-100%"
|
||||
hidden
|
||||
> </i
|
||||
><!
|
||||
[endif]--></span
|
||||
></a
|
||||
>
|
||||
</div>
|
||||
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||
{{ 'workflow.send_external_message.document_available_until'|trans({ 'expiration': send.expireAt}, null, lang) }}
|
||||
</div>
|
||||
<div style="font-weight:normal;padding:16px 24px 16px 24px">
|
||||
{{ 'workflow.send_external_message.or_see_link'|trans }} :
|
||||
</div>
|
||||
<div style="font-size:16px;padding:16px 24px 16px 24px">
|
||||
<code>{{ absolute_url(path('chill_main_workflow_send_view_public', {'uuid': send.uuid, 'verificationKey': send.privateToken})) }}</code>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ 'workflow.public_link.expired_link_title'|trans }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ 'workflow.public_link.expired_link_title'|trans }}</h1>
|
||||
|
||||
<p>{{ 'workflow.public_link.expired_link_explanation'|trans }}</p>
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" >
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" >
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
{% block head_custom %}{% endblock %}
|
||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||
|
||||
{{ encore_entry_link_tags('mod_bootstrap') }}
|
||||
{{ encore_entry_link_tags('mod_forkawesome') }}
|
||||
{{ encore_entry_link_tags('chill') }}
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<nav class="navbar navbar-dark bg-primary navbar-expand-md">
|
||||
<div class="container-xxl">
|
||||
|
||||
<div class="col-4">
|
||||
<a class="navbar-brand" href="{{ path('chill_main_homepage') }}">
|
||||
{{ include('@ChillMain/Layout/_header-logo.html.twig') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-8"></div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="content" id="content">
|
||||
<div class="container-xxl">
|
||||
<div class="row justify-content-center my-5">
|
||||
<div>
|
||||
{% block public_content %}{% endblock %}
|
||||
</div>
|
||||
<div style="margin-top: 1rem;">
|
||||
<p>
|
||||
{{ 'workflow.public_link.shared_explanation_until_remaining'|trans({'expireAt': send.expireAt, 'viewsCount': metadata.viewsCount, 'viewsRemaining': metadata.viewsRemaining}) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ include('@ChillMain/Layout/_footer.html.twig', {'public_page': true}) }}
|
||||
|
||||
{{ encore_entry_script_tags('mod_bootstrap') }}
|
||||
{{ encore_entry_script_tags('mod_forkawesome') }}
|
||||
{{ encore_entry_script_tags('chill') }}
|
||||
|
||||
{% block js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
@@ -0,0 +1,20 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block title %}{{ 'workflow.signature.cancel_signature_of'|trans({ '%signer%': signature.signer|chill_entity_render_string }) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
<p>{{ 'workflow.signature.cancel_are_you_sure'|trans({'%signer%': signature.signer|chill_entity_render_string}) }}</p>
|
||||
|
||||
{{ form_start(form) }}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_main_workflow_show', {'id': signature.step.entityWorkflow.id}) }}">{{ 'Cancel'|trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ form_widget(form.confirm, {'attr': {'class': 'btn btn-misc'}}) }}
|
||||
</li>
|
||||
</ul>
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
@@ -0,0 +1,20 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block title %}{{ 'workflow.signature.reject_signature_of'|trans({ '%signer%': signature.signer|chill_entity_render_string }) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
<p>{{ 'workflow.signature.reject_are_you_sure'|trans({'%signer%': signature.signer|chill_entity_render_string}) }}</p>
|
||||
|
||||
{{ form_start(form) }}
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_main_workflow_show', {'id': signature.step.entityWorkflow.id}) }}">{{ 'Cancel'|trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ form_widget(form.confirm, {'attr': {'class': 'btn btn-misc'}}) }}
|
||||
</li>
|
||||
</ul>
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
@@ -49,19 +49,19 @@
|
||||
|
||||
{% for flashMessage in app.session.flashbag.get('success') %}
|
||||
<div class="alert alert-success flash_message">
|
||||
<span>{{ flashMessage|raw }}</span>
|
||||
<span>{{ flashMessage|trans }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for flashMessage in app.session.flashbag.get('error') %}
|
||||
<div class="alert alert-danger flash_message">
|
||||
<span>{{ flashMessage|raw }}</span>
|
||||
<span>{{ flashMessage|trans }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for flashMessage in app.session.flashbag.get('notice') %}
|
||||
<div class="alert alert-warning flash_message">
|
||||
<span>{{ flashMessage|raw }}</span>
|
||||
<span>{{ flashMessage|trans }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
43
src/Bundle/ChillMainBundle/Routing/ChillUrlGenerator.php
Normal file
43
src/Bundle/ChillMainBundle/Routing/ChillUrlGenerator.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Routing;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
final readonly class ChillUrlGenerator implements ChillUrlGeneratorInterface
|
||||
{
|
||||
public function __construct(private UrlGeneratorInterface $urlGenerator, private RequestStack $requestStack) {}
|
||||
|
||||
public function generate(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
|
||||
{
|
||||
return $this->urlGenerator->generate($name, $parameters, $referenceType);
|
||||
}
|
||||
|
||||
public function generateWithReturnPath(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
|
||||
{
|
||||
$uri = $this->requestStack->getCurrentRequest()->getRequestUri();
|
||||
|
||||
return $this->urlGenerator->generate($name, [...$parameters, 'returnPath' => $uri], $referenceType);
|
||||
}
|
||||
|
||||
public function returnPathOr(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
|
||||
{
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
|
||||
if ($request->query->has('returnPath')) {
|
||||
return $request->query->get('returnPath');
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate($name, $parameters, $referenceType);
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Routing;
|
||||
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* Interface for generating URLs with returnPath information.
|
||||
*/
|
||||
interface ChillUrlGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* Generate an URL without any return path. This is the same as using @see{UrlGeneratorInterface::generate}.
|
||||
*/
|
||||
public function generate(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string;
|
||||
|
||||
/**
|
||||
* Generate an url, append a return path in it.
|
||||
*/
|
||||
public function generateWithReturnPath(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string;
|
||||
|
||||
/**
|
||||
* Get the return path or, if any, generate an url.
|
||||
*/
|
||||
public function returnPathOr(string $name, array $parameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string;
|
||||
}
|
@@ -67,6 +67,10 @@ class AdminUserMenuBuilder implements LocalMenuBuilderInterface
|
||||
'route' => 'chill_crud_admin_user_index',
|
||||
])->setExtras(['order' => 1040]);
|
||||
|
||||
$menu->addChild('crud.admin_user_group.index.title', [
|
||||
'route' => 'chill_crud_admin_user_group_index',
|
||||
])->setExtras(['order' => 1042]);
|
||||
|
||||
$menu->addChild('User jobs', [
|
||||
'route' => 'chill_crud_admin_user_job_index',
|
||||
])->setExtras(['order' => 1050]);
|
||||
|
@@ -60,7 +60,6 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
|
||||
$nbNotifications = $this->notificationByUserCounter->countUnreadByUser($user);
|
||||
|
||||
// TODO add an icon? How exactly? For example a clock icon...
|
||||
$menu
|
||||
->addChild($this->translator->trans('absence.Set absence date'), [
|
||||
'route' => 'chill_main_user_absence_index',
|
||||
@@ -69,6 +68,14 @@ class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
'order' => -8_888_888,
|
||||
]);
|
||||
|
||||
$menu
|
||||
->addChild($this->translator->trans('user_group.my_groups'), [
|
||||
'route' => 'chill_main_user_groups_my',
|
||||
])
|
||||
->setExtras([
|
||||
'order' => -7_777_777,
|
||||
]);
|
||||
|
||||
$menu
|
||||
->addChild(
|
||||
$this->translator->trans('notification.My notifications with counter', ['nb' => $nbNotifications]),
|
||||
|
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Search\Entity;
|
||||
|
||||
use Chill\MainBundle\Repository\UserGroupRepositoryInterface;
|
||||
use Chill\MainBundle\Search\SearchApiInterface;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
|
||||
/**
|
||||
* Provide search api for user group.
|
||||
*/
|
||||
class SearchUserGroupApiProvider implements SearchApiInterface, LocaleAwareInterface
|
||||
{
|
||||
private string $locale;
|
||||
|
||||
public function __construct(private readonly UserGroupRepositoryInterface $userGroupRepository) {}
|
||||
|
||||
public function setLocale(string $locale): void
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
public function getResult(string $key, array $metadata, float $pertinence)
|
||||
{
|
||||
return $this->userGroupRepository->find($metadata['id']);
|
||||
}
|
||||
|
||||
public function prepare(array $metadatas): void {}
|
||||
|
||||
public function provideQuery(string $pattern, array $parameters): SearchApiQuery
|
||||
{
|
||||
return $this->userGroupRepository->provideSearchApiQuery($pattern, $this->getLocale(), 'user-group');
|
||||
}
|
||||
|
||||
public function supportsResult(string $key, array $metadatas): bool
|
||||
{
|
||||
return 'user-group' === $key;
|
||||
}
|
||||
|
||||
public function supportsTypes(string $pattern, array $types, array $parameters): bool
|
||||
{
|
||||
return in_array('user-group', $types, true);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user