enabled workflow module - remains to configure it

This commit is contained in:
Bachir Soussi Chiadmi
2018-02-06 17:13:21 +01:00
parent 7f41d59786
commit d78ebfb03a
93 changed files with 13902 additions and 0 deletions
@@ -0,0 +1,7 @@
Changes since 8.x-1.x
Issue #2351299 by johnv: Porting D7-version to D8 - numerous commits.
Issue #2585093 by johnv: Fixed lost error message on Workflow UI - Transitions page.
Issue #2587165 by johnv: Port Workflow to D8: dropped features, clone support.
Issue #297898: Move "Workflow tab permissions" to permissions page'
Issue #2587171: Make permission "schedule workflow transition" specific per Workflow type
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1 @@
workflow_states_per_page: 20
@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- workflow
theme:
- bartik
id: workflowtransitionform
theme: bartik
region: sidebar_second
weight: -7
provider: null
plugin: workflow_transition_form_block
settings:
id: workflow_transition_form_block
label: 'Workflow Transition form'
provider: workflow
label_display: visible
visibility: { }
@@ -0,0 +1,21 @@
langcode: nl
status: true
dependencies:
module:
- action
- node
- workflow
id: change_a_node_to_next_workflow_state
label: 'Change a node to next Workflow state'
type: node
plugin: workflow_node_next_state_action
configuration:
label: 'Change a node to next Workflow state'
id: change_a_node_to_next_workflow_state
plugin: workflow_node_next_state_action
type: node
workflow_scheduling:
scheduled: '0'
comment: 'New state is set by a triggered Action.'
force: 0
actions: { }
@@ -0,0 +1,713 @@
langcode: en
status: true
dependencies:
module:
- workflow
id: workflow_entity_history
label: 'Workflow Entity history'
module: views
description: 'Enable this View to configure the history tab.'
tag: ''
base_table: workflow_transition_history
base_field: hid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: table
options:
grouping: { }
row_class: ''
default_row_class: true
override: true
sticky: false
caption: ''
summary: ''
description: ''
columns:
hid: hid
timestamp: timestamp
field_name: field_name
from_sid: from_sid
to_sid: to_sid
uid: uid
comment: comment
operations: operations
info:
hid:
default_sort_order: desc
align: ''
separator: ''
empty_column: false
responsive: ''
timestamp:
align: ''
separator: ''
empty_column: false
responsive: ''
field_name:
align: ''
separator: ''
empty_column: false
responsive: ''
from_sid:
align: ''
separator: ''
empty_column: false
responsive: ''
to_sid:
align: ''
separator: ''
empty_column: false
responsive: ''
uid:
align: ''
separator: ''
empty_column: false
responsive: ''
comment:
align: ''
separator: ''
empty_column: false
responsive: ''
operations:
align: ''
separator: ''
empty_column: false
responsive: ''
default: hid
empty_table: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
hid:
id: hid
table: workflow_transition_history
field: hid
relationship: none
group_type: group
admin_label: ''
label: 'Transition ID'
exclude: true
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: hid
plugin_id: field
timestamp:
id: timestamp
table: workflow_transition_history
field: timestamp
relationship: none
group_type: group
admin_label: ''
label: Timestamp
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: timestamp
settings:
date_format: medium
custom_date_format: ''
timezone: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: timestamp
plugin_id: field
field_name:
id: field_name
table: workflow_transition_history
field: field_name
relationship: none
group_type: group
admin_label: ''
label: 'Field name'
exclude: true
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings:
link_to_entity: false
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: field_name
plugin_id: field
from_sid:
id: from_sid
table: workflow_transition_history
field: from_sid
relationship: none
group_type: group
admin_label: ''
label: 'From state'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: from_sid
plugin_id: field
to_sid:
id: to_sid
table: workflow_transition_history
field: to_sid
relationship: none
group_type: group
admin_label: ''
label: 'To state'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: to_sid
plugin_id: field
uid:
id: uid
table: workflow_transition_history
field: uid
relationship: none
group_type: group
admin_label: ''
label: 'User ID'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: uid
plugin_id: field
comment:
id: comment
table: workflow_transition_history
field: comment
relationship: none
group_type: group
admin_label: ''
label: 'Log message'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: basic_string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: comment
plugin_id: field
operations:
id: operations
table: workflow_transition_history
field: operations
relationship: none
group_type: group
admin_label: ''
label: Operations
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
destination: true
entity_type: null
entity_field: null
plugin_id: entity_operations
filters: { }
sorts: { }
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
entity_id:
id: entity_id
table: workflow_transition_history
field: entity_id
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
default_argument_skip_url: false
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
break_phrase: false
not: false
entity_type: workflow_transition
entity_field: entity_id
plugin_id: numeric
display_extenders: { }
title: 'Workflow history'
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
tags: { }
workflow_history_tab:
display_plugin: embed
id: workflow_history_tab
display_title: 'Workflow history tab'
position: 1
display_options:
display_extenders: { }
display_description: 'Replaces the transition list in the Workflow tab.'
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
tags: { }
@@ -0,0 +1,82 @@
# Schema for the configuration files of the Workflow module.
workflow.settings:
type: config_object
label: 'Workflow settings'
mapping:
workflow_states_per_page:
type: integer
label: 'Number of workflow states per page'
workflow.workflow.*:
type: config_entity
label: 'Workflow settings'
mapping:
id:
type: string
label: 'ID'
status:
type: boolean
label: 'Status'
label:
type: label
label: 'Label'
module:
type: string
label: 'Module'
options:
type: mapping
label: 'Options'
mapping:
name_as_title:
type: integer
label: 'Use the workflow name as the title of the workflow form'
fieldset:
type: integer
label: 'Show the form in a fieldset'
options:
type: string
label: 'How to show the available states'
schedule_timezone:
type: integer
label: 'Show a timezone when scheduling a transition'
comment_log_node:
type: integer
label: 'Show comment on the Content edit form'
watchdog_log:
type: integer
label: 'Log watchdog messages upon state change'
field.storage_settings.workflow:
type: mapping
label: 'Workflow settings'
mapping:
workflow_type:
type: string
label: 'Workflow'
allowed_values:
type: sequence
label: 'Allowed values list'
sequence:
type: mapping
label: 'Allowed value with label'
mapping:
value:
type: string
label: 'Value'
label:
type: label
label: 'Label'
allowed_values_function:
type: string
label: 'Allowed values function'
field.field_settings.workflow:
type: mapping
label: 'Workflow settings'
workflow.settings:
type: config_object
mapping:
workflow_states_per_page:
type: integer
label: 'Number of workflow states displayed per page.'
@@ -0,0 +1,27 @@
# Schema for the configuration files of the state from Workflow module.
workflow.state.*:
type: config_entity
label: 'Workflow State'
mapping:
id:
type: string
label: 'ID'
status:
type: boolean
label: 'Status'
label:
type: label
label: 'Label'
module:
type: string
label: 'Module'
wid:
type: string
label: 'Machine name of the attached Workflow'
weight:
type: integer
label: 'Weight'
sysid:
type: integer
label: 'SysId'
@@ -0,0 +1,27 @@
# Schema for the configuration files of the transitions for Workflow module.
workflow.transition.*:
type: config_entity
label: 'Workflow Transition'
mapping:
id:
type: string
label: 'ID'
label:
type: label
label: 'Label'
module:
type: string
label: 'Module'
from_sid:
type: string
label: 'From workflow state'
to_sid:
type: string
label: 'To workflow state'
roles:
type: sequence
label: 'User roles'
sequence:
type: string
label: 'Role'
@@ -0,0 +1,4 @@
# Schema for the views plugins of the Workflow module.
views.filter.workflow_state:
type: views.filter.many_to_one
@@ -0,0 +1,6 @@
workflow_access.settings:
type: config_object
label: Settings
mapping:
workflow_access_priority:
type: integer
@@ -0,0 +1,143 @@
<?php
namespace Drupal\workflow_access\Form;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\workflow\Entity\WorkflowState;
use Drupal\workflow_ui\Form\WorkflowConfigTransitionFormBase;
/**
* Provides the base form for workflow add and edit forms.
*/
class WorkflowAccessRoleForm extends WorkflowConfigTransitionFormBase {
/**
* {@inheritdoc}
*/
protected $entitiesKey = 'workflow_state';
/**
* The WorkflowConfigTransition form type.
*
* @var string
*/
protected $type = 'access';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workflow_access_role';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['workflow_access.role'];
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header = [
'label_new' => t('State'),
'view' => t('Roles who can view posts in this state'),
'update' => t('Roles who can edit posts in this state'),
'delete' => t('Roles who can delete posts in this state'),
];
return $header;
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row = [];
$workflow = $this->workflow;
if ($workflow) {
/* @var $state WorkflowState */
$state = $entity;
$sid = $state->id();
// A list of role names keyed by role ID, including the 'author' role.
// Only get the roles with proper permission + Author role.
$type_id = $workflow->id();
$roles = workflow_get_user_role_names("create $type_id workflow_transition");
if ($state->isCreationState()) {
// No need to set perms on creation.
return [];
}
$view = $update = $delete = [];
$count = 0;
foreach (workflow_access_get_workflow_access_by_sid($sid) as $rid => $access) {
$count++;
$view[$rid] = ($access['grant_view']) ? $rid : 0;
$update[$rid] = ($access['grant_update']) ? $rid : 0;
$delete[$rid] = ($access['grant_delete']) ? $rid : 0;
}
// Allow view grants by default for anonymous and authenticated users,
// if no grants were set up earlier.
if (!$count) {
$view = [
AccountInterface::ANONYMOUS_ROLE => AccountInterface::ANONYMOUS_ROLE,
AccountInterface::AUTHENTICATED_ROLE => AccountInterface::AUTHENTICATED_ROLE,
];
}
$row['label_new'] = [
'#type' => 'value',
'#markup' => t('@label', ['@label' => $state->label()]),
];
$row['view'] = [
'#type' => 'checkboxes',
'#options' => $roles,
'#default_value' => $view,
];
$row['update'] = [
'#type' => 'checkboxes',
'#options' => $roles,
'#default_value' => $update,
];
$row['delete'] = [
'#type' => 'checkboxes',
'#options' => $roles,
'#default_value' => $delete,
];
}
return $row;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->getValue($this->entitiesKey) as $sid => $access) {
// @todo: not waterproof; can be done smarter, using elementchildren().
if (!WorkflowState::load($sid)) {
continue;
}
foreach ($access['view'] as $rid => $checked) {
$data[$rid] = [
'grant_view' => (!empty($access['view'][$rid])) ? (bool) $access['view'][$rid] : 0,
'grant_update' => (!empty($access['update'][$rid])) ? (bool) $access['update'][$rid] : 0,
'grant_delete' => (!empty($access['delete'][$rid])) ? (bool) $access['delete'][$rid] : 0,
];
}
workflow_access_insert_workflow_access_by_sid($sid, $data);
// Update all nodes to reflect new settings.
node_access_needs_rebuild(TRUE);
}
parent::submitForm($form, $form_state);
}
}
@@ -0,0 +1,74 @@
<?php
namespace Drupal\workflow_access\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides the base form for workflow add and edit forms.
*/
class WorkflowAccessSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workflow_access_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['workflow_access.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('workflow_access.settings');
$weight = $config->get('workflow_access_priority');
$form['workflow_access'] = [
'#type' => 'details',
'#open' => TRUE,
'#title' => t('Workflow Access Settings'),
];
$form['workflow_access']['#tree'] = TRUE;
$url = 'https://api.drupal.org/api/drupal/core%21modules%21node%21node.api.php/function/hook_node_access_records/8';
$form['workflow_access']['workflow_access_priority'] = [
'#type' => 'weight',
'#delta' => 10,
'#title' => t('Workflow Access Priority'),
'#default_value' => $weight,
'#description' => t('This sets the node access priority. Changing this
setting can be dangerous. If there is any doubt, leave it at 0.
<a href=":url" target="_blank">Read the manual</a>.', [':url' => $url]),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$weight = $form_state->getValues()['workflow_access']['workflow_access_priority'];
$this->config('workflow_access.settings')
->set('workflow_access_priority', $weight)
->save();
parent::submitForm($form, $form_state);
}
}
@@ -0,0 +1,13 @@
name: 'Workflow access'
description: 'Content access control based on workflows and roles. Depends on the node_access system, so only works for entities of type ''node''.'
dependencies:
- workflow
package: Workflow
# core: 8.x
type: module
# Information added by Drupal.org packaging script on 2017-12-17
version: '8.x-1.0'
core: '8.x'
project: 'workflow'
datestamp: 1513552400
@@ -0,0 +1,41 @@
<?php
/**
* @file
* Workflow access installation.
*/
/**
* Implements hook_schema().
*/
function workflow_access_schema() {
// The D7-table 'workflow_access' is not used anymore.
// The data is no in table 'config'.
return [];
}
/**
* Force rebuild of node access.
*/
function workflow_access_uninstall() {
node_access_needs_rebuild(TRUE);
}
/**
* Force rebuild of node access.
*/
function workflow_access_update_8001(&$sandbox) {
// Rebuild data.
node_access_needs_rebuild(TRUE);
}
/**
* Remove table workflow_access. All current settings are lost!
*/
function workflow_access_update_8002(&$sandbox) {
// Remove the table. Data is now in config.
$schema = \Drupal\Core\Database\Database::getConnection()->schema();
$schema->dropTable('workflow_access');
// Rebuild data.
node_access_needs_rebuild(TRUE);
}
@@ -0,0 +1,15 @@
# Menu tasks for Workflow UI.
workflow_access_settings:
route_name: workflow.access.settings
base_route: entity.workflow_type.collection
title: 'Access settings'
weight: 5
appears_on:
- entity.workflow_type.collection
entity.workflow_type.access_form:
title: 'Access'
route_name: entity.workflow_type.access_form
base_route: entity.workflow_type.edit_form
weight: -1
@@ -0,0 +1,300 @@
<?php
/**
* @file
* Provides node access permissions based on workflow states.
*/
use Drupal\Core\Config\Config;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\Entity\Role;
use Drupal\workflow\Entity\WorkflowTransition;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
/**
* Implements hook_help().
*/
function workflow_access_help($route_name, RouteMatchInterface $route_match) {
$output = '';
switch ($route_name) {
case 'entity.workflow_type.access_form':
$url = \Drupal\Core\Url::fromRoute('workflow.access.settings');
$output .= t("This page lets you refine the permissions per role and per
workflow state. Although the workflow module allows you to add multiple
workflows to per entity type, Workflow Access supports only one
workflow per entity type."
);
$output .= "<br>";
$output .= t("WARNING: Use of the 'Edit any', 'Edit own', and even 'View
published content' permissions for the content type may override these
access settings. You may disable those permissions or
<a href=':url'>alter the priority of
the Workflow access module</a>.", [':url' => $url->toString()]
);
if (\Drupal::moduleHandler()->moduleExists('og')) {
$output .= '<br>';
$output .= t('WARNING: Organic Groups (OG) is present and may interfere
with these settings.');
//$output .= ' ';
//$url = \Drupal\Core\Url::fromUri('admin/config/group/settings'); // @todo D8: FIXME when OG module is ported.
//$output .= t("In particular, if <a href=':url'>Strict node access
// permissions</a> is enabled, since this may override Workflow access
// settings.", [':url' => $url]);
$output .= t("In particular, if <i>Strict node access
permissions</i> is enabled, since this may override Workflow access
settings.");
}
break;
default:
break;
}
return $output;
}
/**
* Implements hook_workflow_operations().
*
* Create action link for access form on EntityWorkflowUIController::overviewForm.
*/
function workflow_access_workflow_operations($op, EntityInterface $entity = NULL) {
$operations = [];
return $operations;
}
/**
* Implements hook_entity_insert().
*
* We use the Role weight as an id.
* In contrary to content_access module, that uses a 'content_access_roles_gids'
* config setting.
* @todo: determine the best way for D8. See also content_access.module
* The problem is that node_access table uses Int, whereas the Role Id is string.
*/
function workflow_access_user_role_insert(EntityInterface $entity) {
// Attend user to Rebuild data, because the weight of a role
// is the key for workflow_Access.
/** @var $entity \Drupal\user\RoleInterface */
node_access_needs_rebuild(TRUE);
}
/**
* Implements hook_access_entity_update().
*
* @param EntityInterface $entity
*/
function workflow_access_user_role_update(EntityInterface $entity) {
// Attend user to Rebuild data, because the weight of a role
// is the key for workflow_Access.
/** @var $entity \Drupal\user\RoleInterface */
if ($entity->getWeight() != $entity->original->getWeight()) {
// Role's weight has changed.
node_access_needs_rebuild(TRUE);
}
}
/**
* Implements hook_node_grants().
*
* Supply the workflow access grants. We are simply using
* roles as access lists, so rids translate directly to gids.
*/
function workflow_access_node_grants(AccountInterface $account, $op) {
$gids = [];
$roles = $account->getRoles();
foreach ($roles as $role) {
// @todo D8: compare with content_access module.
$gids[] = workflow_access_get_role_gid($role);
}
return [
'workflow_access' => $gids,
'workflow_access_owner' => [$account->id()],
];
}
/**
* Helper providing numeric id for role.
* Copied from content_access.module
*/
function workflow_access_get_role_gid($rid) {
// @todo D8: compare with content_access module.
// $config = \Drupal::configFactory()->getEditable('content_access.settings');
// $roles_gids = $config->get('content_access_roles_gids');
// return $roles_gids[$role];
/* @var $role Role */
$role = \Drupal\user\Entity\Role::load($rid);
$weight = 100; // Avoid negative values.
if ($role) {
$weight += $role->getWeight();
}
else {
// For Author, no role exists.
$weight -= 20;
}
return $weight;
}
/**
* Implements hook_node_access_records().
*
* Returns a list of grant records for the passed in node object.
* This hook is invoked by function node_access_acquire_grants().
*/
function workflow_access_node_access_records(\Drupal\node\NodeInterface $node) {
$grants = [];
$workflow_transitions_added = FALSE;
//if (!$node->isPublished()) {
// // @todo: return;
//}
$entity_type = $node->getEntityTypeId();
// Only relevant for content with Workflow.
if (!isset($node->workflow_transitions)) {
$node->workflow_transitions = [];
// Sometimes, a node is saved without going through workflow_transition_form.
// E.g.,
// - when saving a node with workflow_node programmatically with node_save();
// - when changing a state on a node view page/history tab;
// - when rebuilding permissions via batch for Workflow Fields.
// In that case, we need to create the workflow_transitions ourselves to
// calculate the grants.
foreach (_workflow_info_fields($node, $entity_type) as $field) {
$field_name = $field->getName(); // Do not use id().
$old_sid = $new_sid = $node->$field_name->value;
// Create a dummy transition, just to set $node->workflow_transitions.
if ($old_sid) {
/* @var $transition WorkflowTransitionInterface */
$transition = WorkflowTransition::create([$old_sid, 'field_name' => $field_name]);
$transition->setTargetEntity($node);
$transition->setValues($new_sid, $node->getOwnerId(), \Drupal::time()->getRequestTime(), '');
// Store the transition, so it can be easily fetched later on.
// Store in an array, to prepare for multiple workflow_fields per entity.
// This is a.o. used in hook_entity_update to trigger 'transition post'.
$node->workflow_transitions[$field_name] = $transition;
$workflow_transitions_added = TRUE;
}
}
}
// Get 'author' of this entity.
// - Some entities (e.g., taxonomy_term) do not have a uid.
// But then again: node_access is only for nodes...
$uid = ($node->getOwnerId()) ? (int) $node->getOwnerId() : 0;
$priority = workflow_access_get_setting('workflow_access_priority');
$count = 0;
foreach ($node->workflow_transitions as $transition) {
// @todo: add support for +1 workflows per entity.
if ($count++ == 1) {
continue;
}
$field_name = $transition->getFieldName();
if ($current_sid = workflow_node_current_state($node, $field_name)) {
foreach (workflow_access_get_workflow_access_by_sid($current_sid) as $rid => $grant) {
$realm = ($uid > 0 && $rid == WORKFLOW_ROLE_AUTHOR_RID) ? 'workflow_access_owner' : 'workflow_access';
$gid = ($uid > 0 && $rid == WORKFLOW_ROLE_AUTHOR_RID) ? $uid : workflow_access_get_role_gid($rid);
// Anonymous ($uid == 0) author is not allowed for role 'author' (== -1).
// Both logically (Anonymous having more rights then authenticated)
// and technically: $gid must be a positive int.
if ($uid == 0 && $rid == WORKFLOW_ROLE_AUTHOR_RID) {
continue;
}
$grants[] = [
'realm' => $realm,
'gid' => $gid,
'grant_view' => (int) $grant['grant_view'],
'grant_update' => (int) $grant['grant_update'],
'grant_delete' => (int) $grant['grant_delete'],
'priority' => $priority,
// 'langcode' => $node->language(),
'field_name' => $field_name, // Just for analysis and info.
];
}
}
}
if ($workflow_transitions_added == TRUE) {
unset($node->workflow_transitions);
}
return $grants;
}
/**
* Implements hook_node_access_explain().
*
* This is a Devel Node Access hook.
*/
function workflow_access_node_access_explain($row) {
static $interpretations = [];
switch ($row->realm) {
case 'workflow_access_owner':
$interpretations[$row->gid] = t('Workflow access: author of the content may access');
break;
case 'workflow_access':
$roles = user_roles();
$interpretations[$row->gid] = t('Workflow access: %role may access', ['%role' => $roles[$row->gid]]);
break;
}
return (!empty($interpretations[$row->gid]) ? $interpretations[$row->gid] : NULL);
}
/**
* DB functions - all DB interactions are isolated here to make for easy updating should our schema change.
*/
/**
* Given a sid, retrieve the access information and return the row(s).
*/
function workflow_access_get_workflow_access_by_sid($sid) {
$result = \Drupal::config('workflow_access.role')->get($sid);
if ($result == NULL) {
// Avoid errors in calling function when no data available.
$result = [];
}
return $result;
}
/**
* Given a sid, delete all access data for this state.
*/
function workflow_access_delete_workflow_access_by_sid($sid) {
\Drupal::configFactory()->getEditable('workflow_access.role')
->clear($sid)
->save();
}
/**
* Given data, insert into workflow access - we never update.
*/
function workflow_access_insert_workflow_access_by_sid($sid, &$data, Config $config = NULL) {
$config = \Drupal::configFactory()->getEditable('workflow_access.role');
$config
->set($sid, $data)
->save();
}
/**
* Get the module settings.
*
* @see WorkflowAccessSettingsForm::submitForm()
*/
function workflow_access_get_setting($value) {
$config = \Drupal::config('workflow_access.settings');
$priority = $config->get($value);
return $priority;
}
@@ -0,0 +1,21 @@
# Declaration of Workflow Access routings.
### Workflow settings form
workflow.access.settings:
path: '/admin/config/workflow/workflow/access'
defaults:
_form: '\Drupal\workflow_access\Form\WorkflowAccessSettingsForm'
_title: 'Access settings'
requirements:
_permission: 'administer workflow'
_module_dependencies: 'workflow_ui'
entity.workflow_type.access_form:
path: '/admin/config/workflow/workflow/{workflow_type}/access'
defaults:
_title: 'Edit Workflow'
_title_callback: 'workflow_url_get_title'
_form: '\Drupal\workflow_access\Form\WorkflowAccessRoleForm'
requirements:
_permission: 'administer workflow'
_module_dependencies: 'workflow_ui'
@@ -0,0 +1,138 @@
<?php
namespace Drupal\workflow_cleanup\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflow\Entity\WorkflowConfigTransition;
use Drupal\workflow\Entity\WorkflowState;
/**
* Class WorkflowCleanupSettingsForm
*
* @package Drupal\workflow_cleanup\Form
*/
class WorkflowCleanupSettingsForm extends FormBase {
/**
* @inheritdoc
*/
public function getFormId() {
return 'workflow_cleanup_settings';
}
/**
* @inheritdoc
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = [];
// Get all of the states, indexed by sid.
$orphans = $inactive = [];
/* @var $states WorkflowState[] */
/* @var $state WorkflowState */
$states = WorkflowState::loadMultiple();
foreach ($states as $state) {
// Does the associated workflow exist?
if (!$state->getWorkflow()) {
$orphans[$state->id()] = $state;
}
else {
// Is the state still active?
if (!$state->isActive()) {
$inactive[$state->id()] = $state;
}
}
}
// Save the relevant states in an indexed array.
$form['#workflow_states'] = $orphans + $inactive;
$form['no_workflow'] = [
'#type' => 'details',
'#title' => t('Orphaned States'),
'#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE.
'#description' => t('These states no longer belong to an existing workflow.'),
'#tree' => TRUE,
];
foreach ($orphans as $sid => $state) {
$form['no_workflow'][$sid]['check'] = [
'#type' => 'checkbox',
'#title' => $state->label(),
'#return_value' => $sid,
];
}
$form['inactive'] = [
'#type' => 'details',
'#title' => t('Inactive (Deleted) States'),
'#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE.
'#description' => t('These states belong to a workflow, but have been marked inactive (deleted).'),
'#tree' => TRUE,
];
foreach ($inactive as $sid => $state) {
$form['inactive'][$sid]['check'] = [
'#type' => 'checkbox',
'#title' => $state->label() . ' (' . $state->getWorkflow()->label() . ')',
'#return_value' => $sid,
];
}
$form['submit'] = [
'#type' => 'submit',
'#value' => t('Delete selected states'),
];
return $form;
}
/**
* @inheritdoc
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$states = $form['#workflow_states'];
$values = $form_state->getValues();
foreach (['no_workflow', 'inactive'] as $section) {
if (!isset($values[$section])) {
continue;
}
foreach ($values[$section] as $sid => $data) {
if ($data['check']) {
/* @var $state WorkflowState */
$state = $states[$sid];
$state_name = $state->label();
// Delete any transitions this state is involved in.
$count = 0;
foreach (WorkflowConfigTransition::loadMultiple() as $config_transition) {
/* @var $config_transition WorkflowConfigTransition */
if ($config_transition->getFromSid() == $sid || $config_transition->getToSid() == $sid) {
$config_transition->delete();
$count++;
}
}
if ($count) {
drupal_set_message(t('@count transitions for the "@state" state have been deleted.',
['@state' => $state_name, '@count' => $count]));
}
// @todo: Remove history records too.
$count = 0;
// $count = db_delete('workflow_node_history')->condition('sid', $sid)->execute();
if ($count) {
drupal_set_message(t('@count history records for the "@state" state have been deleted.',
['@state' => $state_name, '@count' => $count]));
}
$state->delete();
drupal_set_message(t('The "@state" state has been deleted.',
['@state' => $state_name]));
}
}
}
}
}
@@ -0,0 +1,13 @@
name: Workflow Clean Up
type: module
description: 'Cleans up Workflow cruft. (Do not enable in production sites.)'
package: Workflow
# core: 8.x
dependencies:
- workflow
configure: workflow.cleanup.settings
# Information added by Drupal.org packaging script on 2017-12-17
version: '8.x-1.0'
core: '8.x'
project: 'workflow'
datestamp: 1513552400
@@ -0,0 +1,7 @@
# Menu tasks for Workflow Cleanup.
workflow.cleanup.settings:
title: 'Cleanup'
route_name: workflow.cleanup.settings
base_route: entity.workflow_type.collection
weight: 7
@@ -0,0 +1,26 @@
<?php
/**
* @file
* Cleans up Workflow cruft that may build up over time.
*/
/**
* Implements hook_help().
*/
function workflow_cleanup_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match) {
$output = [];
switch ($route_name) {
case 'workflow.cleanup.settings':
$output = t('This page allows you to delete orphaned and inactive states.
States can be deleted freely in a development environment, but be
careful if you have used a State in a production environment. The
transition history of your content will loose the description of a
previously used state. If your Workflow must comply to some auditing
standards, you should NOT use this function.');
break;
}
return $output;
}
@@ -0,0 +1,7 @@
workflow.cleanup.settings:
path: '/admin/config/workflow/workflow/cleanup'
defaults:
_form: '\Drupal\workflow_cleanup\Form\WorkflowCleanupSettingsForm'
_title: 'Clean up workflow'
requirements:
_permission: 'administer workflow'
@@ -0,0 +1,3 @@
The Workflow Devel module helps you to develop addons for Workflow.
When activating this module, you will get a message with every hook, Workflow provides.
@@ -0,0 +1,14 @@
name: 'Workflow Devel'
description: 'Tools for developing Workflow add-ons. Calls every hook in workflow.api.php and generates a user message on each call. (Do not enable in production sites.)'
package: Workflow
type: module
# core: 8.x
dependencies:
- workflow
# Information added by Drupal.org packaging script on 2017-12-17
version: '8.x-1.0'
core: '8.x'
project: 'workflow'
datestamp: 1513552400
@@ -0,0 +1,143 @@
<?php
/**
* @file
* Development tools for Workflow.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\user\UserInterface;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
module_load_include('php', 'workflow', 'workflow.api');
/**
* Hooks defined by workflow_ui, workflow module.
*/
function workflow_devel_workflow_operations($op, EntityInterface $entity = NULL) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, $op, '');
return hook_workflow_operations($op, $entity);
}
/**
* Hooks defined by workflow module.
*/
function workflow_devel_workflow($op, WorkflowTransitionInterface $transition, UserInterface $user) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, $op, '');
return hook_workflow($op, $transition, $user);
}
function workflow_devel_workflow_comment_alter(&$comment, array &$context) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, $comment, '');
return hook_workflow_comment_alter($comment, $context);
}
function workflow_devel_workflow_history_alter(array &$context) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__);
return hook_workflow_history_alter($context);
}
function workflow_devel_workflow_permitted_state_transitions_alter(array &$transitions, array $context) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__);
return hook_workflow_permitted_state_transitions_alter($transitions, $context);
}
/**
* Hooks defined by core Form API: Change the Workflow Transition Form.
*/
/**
* Alter forms for field widgets provided by other modules.
*
* @param $element
* The field widget form element as constructed by hook_field_widget_form().
* @param $form_state
* An associative array containing the current state of the form.
* @param $context
* An associative array containing the following key-value pairs, matching the
* arguments received by hook_field_widget_form():
* - form: The form structure to which widgets are being attached. This may be
* a full form structure, or a sub-element of a larger form.
* - field: The field structure.
* - instance: The field instance structure.
* - langcode: The language associated with $items.
* - items: Array of default values for this field.
* - delta: The order of this item in the array of subelements (0, 1, 2, etc).
*
* @see hook_field_widget_form()
* @see hook_field_widget_WIDGET_TYPE_form_alter()
*/
function workflow_devel_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
// A hook for changing any widget. Better not use it: it is called on EVERY
// Widget. (Even though the message is only shown once.)
// D7: This hook is introduced in Drupal 7.8.
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', '');
// dpm($context['widget']->getPluginId());
return hook_field_widget_form_alter($element, $form_state, $context);
}
function workflow_devel_field_widget_workflow_default_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
// A hook specific for the 'workflow_default' widget.
// D7: This hook is introduced in Drupal 7.8.
// D8: This name is specified in the annotation of WorkflowDefaultWidget.
workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', '');
return hook_field_widget_workflow_default_form_alter($element, $form_state, $context);
}
function workflow_devel_form_workflow_transition_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, $form_id, '');
return hook_form_workflow_transition_form_alter($form, $form_state, $form_id);
}
function workflow_devel_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
if (substr($form_id, 0, 8) == 'workflow') {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, $form_id, '');
}
return hook_form_alter($form, $form_state, $form_id);
}
/**
* Hooks defined by core: Change the operations column in an Entity list.
*
* @see EntityListBuilder::getOperations()
*
* @return array
*/
function workflow_devel_entity_operation($entities) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', '');
$operations = [];
return $operations;
}
function workflow_devel_entity_operation_alter(array $operations, EntityInterface $entity) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, $entity->getEntityTypeId(), $entity->id());
}
/**
* Hooks defined by core: hook_entity_CRUD.
*
* @see hook_entity_create(), hook_entity_update(), etc.
* @see hook_ENTITY_TYPE_create(), hook_ENTITY_TYPE_update(), etc.
*/
function workflow_devel_entity_create(EntityInterface $entity) {
//workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'create' , $entity->getEntityTypeId());
}
function workflow_devel_entity_presave(EntityInterface $entity) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'presave', $entity->getEntityTypeId());
}
function workflow_devel_entity_insert(EntityInterface $entity) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'insert', $entity->getEntityTypeId());
}
function workflow_devel_entity_update(EntityInterface $entity) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'update', $entity->getEntityTypeId());
}
function workflow_devel_entity_predelete(EntityInterface $entity) {
if (substr($entity->getEntityTypeId(), 0, 8) == 'workflow') {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'predelete', $entity->getEntityTypeId());
}
return hook_entity_predelete($entity);
}
function workflow_devel_entity_delete(EntityInterface $entity) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete', $entity->getEntityTypeId());
return hook_entity_delete($entity);
}
@@ -0,0 +1,12 @@
name: 'Workflow Field'
description: 'A residual submodule. No need to enable this module for installations with workflow 8.x-1.0-beta6 and later.'
package: Workflow
type: module
# core: 8.x
hidden: TRUE
# Information added by Drupal.org packaging script on 2017-12-17
version: '8.x-1.0'
core: '8.x'
project: 'workflow'
datestamp: 1513552400
@@ -0,0 +1,133 @@
<?php
namespace Drupal\workflow_operations\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Url;
use Drupal\workflow\Entity\WorkflowTransition;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
class WorkflowTransitionRevertForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workflow_transition_revert_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
/* @var $transition WorkflowTransitionInterface */
$transition = $this->entity;
$state = $transition->getFromState();
if ($state) {
$question = t('Are you sure you want to revert %title to the "@state" state?', [
'@state' => $state->label(),
'%title' => $transition->label(),
]);
return $question;
}
else {
\Drupal::logger('workflow_revert')->error('Invalid state', []);
drupal_set_message(t('Invalid transition. Your information has been recorded.'), 'error');
//drupal_goto($return_uri);
}
return [];
}
public function getCancelUrl() {
/* @var $transition WorkflowTransitionInterface */
$transition = $this->entity;
return new Url('entity.node.workflow_history', ['node' => $transition->getTargetEntityId(), 'field_name' => $transition->getFieldname()]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return t('Revert');
}
/**
* {@inheritdoc}
*/
//public function getDescription() {
// return '';
//}
/**
* The fact that we need to overwrite this function, is an indicator that
* the Transition is not completely a complete Entity.
*
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
//return parent::copyFormValuesToEntity($entity, $form, $form_state);
return $this->entity;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, \Drupal\Core\Form\FormStateInterface $form_state) {
// If Rules is available, signal the reversion.
// @todo: move this Rules_invoke_event to hook outside this module.
if (\Drupal::moduleHandler()->moduleExists('rules')) {
rules_invoke_event('workflow_state_reverted', $this->entity);
}
/* @var $transition WorkflowTransitionInterface */
$transition = $this->prepareRevertedTransition($this->entity);
// The entity will be updated when the transition is executed. Keep the
// original one for the confirmation message.
$previous_sid = $transition->getToSid();
// Force the transition because it's probably not valid.
$transition->force(TRUE);
$new_sid = workflow_execute_transition($transition, TRUE);
$comment = ($previous_sid == $new_sid) ? 'State is reverted.' : 'State could not be reverted.';
drupal_set_message(t($comment), 'warning');
$form_state->setRedirect('entity.node.workflow_history', [
'node' => $transition->getTargetEntityId(),
'field_name' => $transition->getFieldName(),
]
);
}
/**
* Prepares a transition to be reverted.
*
* @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition
* The transition to be reverted.
*
* @return \Drupal\workflow\Entity\WorkflowTransitionInterface
* The prepared transition ready to be stored.
*/
protected function prepareRevertedTransition(WorkflowTransitionInterface $transition) {
$user = \Drupal::currentUser();
$entity = $transition->getTargetEntity();
$field_name = $transition->getFieldName();
$current_sid = workflow_node_current_state($entity, $field_name);
$previous_sid = $transition->getFromSid();
$comment = t('State reverted.');
$transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]);
$transition->setTargetEntity($entity);
$transition->setValues($previous_sid, $user->id(), \Drupal::time()->getRequestTime(), $comment);
return $transition;
}
}
@@ -0,0 +1,144 @@
<?php
namespace Drupal\workflow_operations;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\workflow\Entity\WorkflowManager;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the access control handler for the workflow entity type.
*
* @see \Drupal\workflow\Entity\Workflow
* @ingroup workflow_access
*/
class WorkflowAccessControlHandler extends \Drupal\workflow\WorkflowAccessControlHandler {
/**
* This is a hack.
*
* {@inheritdoc}
*/
public function __construct(EntityTypeInterface $entity_type = NULL) {
if ($entity_type) {
parent::__construct($entity_type);
}
else {
//$this->entityTypeId = $entity_type->id();
$this->entityType = $entity_type;
}
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type
);
}
/**
* Access check, to be called from
* - module.routing.yml
* - hook_entity_operation
*
* @param \Drupal\workflow_operations\WorkflowTransitionInterface|null $transition
*
* @return \Drupal\Core\Access\AccessResult
*/
public function revertAccess(WorkflowTransitionInterface $transition = NULL, AccountInterface $account = NULL, $return_as_object = TRUE) {
if ($transition) {
// Called from hook_entity_operation
}
else {
// Called from module.routing.yml
/* @var $transition WorkflowTransitionInterface */
$route_match = \Drupal::routeMatch();
$transition = $route_match->getParameter('workflow_transition');
}
return $this->access($transition, 'revert', $account, $return_as_object);
}
/**
* {@inheritdoc}
*/
public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::neutral();
$account = $user = workflow_current_user($account);
// This is only for Edit/Delete transition. For Add/create, use createAccess.
switch ($entity->getEntityTypeId()) {
case 'workflow_transition':
case 'workflow_scheduled_transition':
/* @var $transition WorkflowTransitionInterface */
$transition = $entity;
switch ($operation) {
case 'revert':
$is_owner = WorkflowManager::isOwner($user, $transition);
$type_id = $transition->getWorkflowId();
if ($transition->getFromSid() == $transition->getToSid()) {
// No access for same state transitions.
$result = AccessResult::forbidden();
}
elseif ($user->hasPermission("revert any $type_id workflow_transition")) {
// OK, add operation.
$result = AccessResult::allowed();
}
elseif ($is_owner && $user->hasPermission("revert own $type_id workflow_transition")) {
// OK, add operation.
$result = AccessResult::allowed();
}
else {
// No access.
$result = AccessResult::forbidden();
}
break;
default:
$result = parent::access($entity, $operation, $account, $return_as_object)->cachePerPermissions();
break;
} // End of switch ($operation).
break; // case
default: // $entity_type
$result = AccessResult::forbidden();
} // End of switch($entity->getEntityTypeId()).
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
$result = parent::createAccess($entity_bundle, $account, $context, TRUE)->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $transition, $operation, AccountInterface $account) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
return parent::checkAccess($transition, $operation, $account);
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
return AccessResult::allowedIf($account->hasPermission('create ' . $entity_bundle . ' content'))->cachePerPermissions();
}
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\workflow_operations;
use Drupal\workflow\Entity\Workflow;
/**
* Provides dynamic permissions for workflows of different types.
*/
class WorkflowPermissions extends \Drupal\workflow\WorkflowPermissions {
/**
* Returns an array of workflow type permissions.
*
* @return array
* The workflow type permissions.
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
//public function workflowTypePermissions() {
// return parent::workflowTypePermissions();
//}
/**
* Returns a list of workflow permissions for a given workflow type.
*
* @param Workflow|\Drupal\workflow\Entity\Workflow $type
* The workflow type.
* @return array An associative array of permission names and descriptions.
* An associative array of permission names and descriptions.
*/
protected function buildPermissions(Workflow $type) {
$type_id = $type->id();
$type_params = ['%type_name' => $type->label()];
return [
// D7->D8-Conversion of 'edit workflow comment' permission to "edit own $type_id transition" (@see NodePermissions::edit any/own content).
// D7->D8-Conversion of 'edit workflow comment' permission to "edit any $type_id transition" (@see NodePermissions::edit any/own content).
"edit own $type_id workflow_transition" => [
'title' => $this->t('%type_name: Edit own comments', $type_params),
'description' => t('Edit the comment of own executed state transitions.'),
'restrict access' => TRUE,
],
"edit any $type_id workflow_transition" => [
'title' => $this->t('%type_name: Edit any comments', $type_params),
'description' => t('Edit the comment of any executed state transitions.'),
'restrict access' => TRUE,
],
/*
// Workflow has no 'delete' permissions.
"delete own $type_id workflow_transition" => [
'title' => $this->t('%type_name: Delete own content', $type_params),
],
"delete any $type_id workflow_transition" => [
'title' => $this->t('%type_name: Delete any content', $type_params),
],
*/
// D7->D8-Conversion of 'revert workflow' permission to "revert any/own $type_id transition"
"revert own $type_id workflow_transition" => [
'title' => $this->t('%type_name: Revert own state transition', $type_params),
'description' => t('Allow user to revert own last executed state transition on entity.'),
'restrict access' => TRUE,
],
"revert any $type_id workflow_transition" => [
'title' => $this->t('%type_name: Revert any state transition', $type_params),
'description' => t('Allow user to revert any last executed state transition on entity.'),
'restrict access' => TRUE,
],
];
}
}
@@ -0,0 +1,14 @@
name: 'Workflow operations'
description: "Adds lesser-used Transition permissions (edit, revert, delete permissions)."
package: Workflow
type: module
# core: 8.x
dependencies:
- workflow
# Information added by Drupal.org packaging script on 2017-12-17
version: '8.x-1.0'
core: '8.x'
project: 'workflow'
datestamp: 1513552400
@@ -0,0 +1,97 @@
<?php
/**
* @file
* Adds an 'Revert' link to the first workflow history row.
*/
use Drupal\workflow\Entity\WorkflowTransitionInterface;
use Drupal\workflow_operations\WorkflowAccessControlHandler;
/**
* Implements hook_rules_event_info().
*
* @todo: Add support for every entity_type in Revert rules.
*/
function workflow_operations_rules_event_info() {
workflow_debug( __FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
$events = [
'workflow_state_reverted' => [
'group' => t('Workflow'),
'label' => t('Workflow state reverted'),
'variables' => rules_events_node_variables(t('updated content'), TRUE),
],
];
return $events;
}
/**
* Core hooks: Change the operations column in a Entity list.
* Add a 'revert' operation.
*
* @see EntityListBuilder::getOperations()
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*
* @return array
*/
function workflow_operations_entity_operation(\Drupal\Core\Entity\EntityInterface $entity) {
$operations = [];
// Check correct entity type.
if (!in_array($entity->getEntityTypeId(), ['workflow_transition'])) {
return $operations;
}
$user = workflow_current_user();
/* @var $transition WorkflowTransitionInterface */
$transition = $entity;
// Check access, to avoid that user sees a revert link, but is not allowed to
// view the revert form. Use $first to check only once per page.
static $first = TRUE;
if ($first) {
$handler = new WorkflowAccessControlHandler();
$is_allowed = $handler->access($transition, 'revert', $user, FALSE);
if (!$is_allowed) {
// No access. We may try at next row.
return $operations;
}
}
// Only mark the first row.
if ($first) {
// Some states are not fit to revert to. In each of these cases, prohibit
// to revert to an even older state.
if ((!$from_state = $transition->getFromState()) || !$from_state->isActive() || $from_state->isCreationState()) {
$first = FALSE;
}
else {
// Let's ask other modules if the reversion is allowed. Reversing old and new sid!
$permitted = \Drupal::moduleHandler()->invokeAll('workflow', ['transition revert', $transition, $user]); // @todo: add revert operation.
// Stop if a module says so.
if (!in_array(FALSE, $permitted, TRUE)) {
// Add the operation - it is not vetoed.
// @todo: Pluggability problem: the revert_form route is fixed in WorkflowTransition Annotation. Add to some alter() function.
$operations['revert'] = [
'title' => t('Revert to last state'),
'url' => \Drupal\Core\Url::fromRoute('entity.workflow_transition.revert_form', ['workflow_transition' => $transition->id()]),
'query' => \Drupal::destination()->getAsArray(), // Add destination.
'weight' => 50,
];
$first = FALSE;
}
}
}
return $operations;
}
/**
* Implements hook_entity_operation_alter
* This hook is not needed in this case.
*/
//function workflow_operations_entity_operation_alter(array $operations, \Drupal\Core\Entity\EntityInterface $entity) {
//}
@@ -0,0 +1,2 @@
permission_callbacks:
- \Drupal\workflow_operations\WorkflowPermissions::workflowTypePermissions
@@ -0,0 +1,10 @@
### Workflow Transition CRUD
entity.workflow_transition.revert_form:
path: '/workflow_transition/{workflow_transition}/revert'
defaults:
_entity_form: 'workflow_transition.revert'
_title: 'Revert Workflow transition'
requirements:
# _entity_access: 'workflow_transition.revert'
_custom_access: '\Drupal\workflow_operations\WorkflowAccessControlHandler::revertAccess'
# _access: 'true'
@@ -0,0 +1,104 @@
<?php
namespace Drupal\workflow_ui\Controller;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
/**
* Defines a class to build a listing of Workflow entities.
*
* @see \Drupal\workflow\Entity\Workflow
*/
class WorkflowListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
public function getFormId() {
// return 'workflow_form';
return parent::getFormId();
}
/**
* {@inheritdoc}
*
* Building the header and content lines for the contact list.
*
* Calling the parent::buildHeader() adds a column for the possible actions
* and inserts the 'edit' and 'delete' links as defined for the entity type.
*/
public function buildHeader() {
$header['id'] = $this->t('ID');
$header['label'] = $this->t('Label');
$header['status'] = $this->t('Status');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/* @var $entity \Drupal\workflow\Entity\Workflow */
$row['id'] = $entity->id();
$row['label'] = $this->getLabel($entity);
$row['status'] = ''; // TODO $entity->getStatus();
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
/* @var $workflow \Drupal\workflow\Entity\Workflow */
$workflow = $entity;
// Do not delete a Workflow if it contains content.
if (isset($operations['delete']) && !$workflow->isDeletable()) {
unset ($operations['delete']);
}
/**
* Allow modules to insert their own workflow operations to the list.
*/
// This is what EntityListBuilder::getOperations() does:
// $operations = $this->getDefaultOperations($entity);
// $operations += $this->moduleHandler()->invokeAll('entity_operation', [$entity]);
// $this->moduleHandler->alter('entity_operation', $operations, $entity);
// In D8, the interface of below hook_workflow_operations has changed a bit.
// @see EntityListBuilder::getOperations, workflow_operations, workflow.api.php.
$operations += $this->moduleHandler()->invokeAll('workflow_operations', ['workflow', $workflow]);
return $operations;
}
/**
* {@inheritdoc}
*/
public function render() {
$build = parent::render();
/**
* Allow modules to insert their own top_action links to the list, like cleanup module.
*
* This is not done anymore via the workflow hook.
* Instead, for an example:
* @see workflow_ui.links.action.yml
* @see workflow.api.php under 'hook_workflow_operations'.
*/
// $top_actions = \Drupal::moduleHandler()
// ->invokeAll('workflow_operations', ['top_actions', NULL]);
// $top_actions_args = [
// 'links' => $top_actions,
// 'attributes' => ['class' => ['inline', 'action-links']],
// ];
return $build;
}
}
@@ -0,0 +1,406 @@
<?php
namespace Drupal\workflow_ui\Controller;
use Drupal\Core\Config\Entity\DraggableListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflow\Entity\WorkflowState;
/**
* Defines a class to build a draggable listing of Workflow State entities.
*
* @see \Drupal\workflow\Entity\WorkflowState
*/
class WorkflowStateListBuilder extends DraggableListBuilder {
// @TODO D8-port WorkflowStateListBuilder: enable machine_name: interactive WorkflowState element.
/**
* Load the Transitions, and filter for Workflow type.
* {@inheritdoc}
*/
public function load() {
$entities = [];
// Get the Workflow from the page.
/** @var $workflow \Drupal\workflow\Entity\Workflow */
if (!$workflow = workflow_url_get_workflow()) {
// @todo: Generate error message.
return $entities;
}
$wid = $url_wid = $workflow->id();
$entities = parent::load();
foreach ($entities as $key => $entity) {
if (!isset($entity->wid) || $entity->wid != $wid) {
unset($entities[$key]);
}
}
return $entities;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workflow_state_form';
//return parent::getFormId();
}
/**
* {@inheritdoc}
*
* Building the header and content lines for the contact list.
*
* Calling the parent::buildHeader() adds a column for the possible actions
* and inserts the 'edit' and 'delete' links as defined for the entity type.
*/
public function buildHeader() {
// The column 'weight' is added magically in the draggable EntityList.
// $header['weight'] = $this->t('Weight');
// Some columns are not welcome in the list.
// $header['module'] = $this->t('Module');
// $header['wid'] = $this->t('Workflow');
// $header['sysid'] = $this->t('Sysid');
// Column 'label' is manipulated in parent::buildForm(). So, we use 'label_new'.
// $header['label'] = $this->t('Label');
$header['label_new'] = $this->t('Label');
$header['id'] = $this->t('ID');
$header['sysid'] = '';
$header['status'] = $this->t('Active');
$header['reassign'] = $this->t('Reassign');
$header['count'] = $this->t('Count');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row = [];
// Get the Workflow from the page.
/** @var $workflow \Drupal\workflow\Entity\Workflow */
if (!$workflow = workflow_url_get_workflow()) {
workflow_debug( __FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
}
$wid = $url_wid = $workflow->id();
/** @var $entity \Drupal\workflow\Entity\WorkflowState */
$state = $entity;
$sid = $state->id();
$label = $state->label();
$count = $state->count();
// Build select options for reassigning states.
// We put a blank state first for validation.
$state_options = ['' => ' '];
$state_options += workflow_get_workflow_state_names($wid, FALSE);
// Make it impossible to reassign to the same state that is disabled.
if ($state->isCreationState() || !$sid || !$state->isActive()) {
$current_state_options = [];
}
else {
$current_state = [$sid => $state_options[$sid]];
$current_state_options = array_diff($state_options, $current_state);
}
/*
* Build the Row.
*/
// The column 'weight' is added magically in the draggable EntityList.
// $row['weight'] = $state->weight;
// Some columns are not welcome in the list.
// $row['module'] = $state->getModule();
// $row['wid'] = $state->getWorkflow();
// Column 'label' is manipulated in parent::buildForm(). So, we use 'label_new'.
// $row['label'] = $state->label();
$row['label_new'] = [
'#markup' => $label,
'#type' => 'textfield',
'#size' => 30,
'#maxlength' => 255,
'#default_value' => $label,
'#title' => NULL, // This hides the red 'required' asterisk.
// '#required' => TRUE,
];
$row['id'] = [
'#type' => 'machine_name',
'#title' => NULL, // This hides the red 'required' asterisk.
'#size' => 30,
'#description' => NULL,
'#disabled' => TRUE, // !$state->isNew(),
'#default_value' => $state->id(),
// N.B.: Keep machine_name in WorkflowState and ~ListBuilder aligned.
'#required' => FALSE,
// @TODO D8-port: enable machine_name: interactive WorkflowState element.
'#machine_name' => [
'exists' => [$this, 'exists'], // Local helper function, at the bottom of this class.
// 'source' => ['label_new'],
'source' => ['states', $state->id(), 'label_new'],
// 'replace_pattern' =>'([^a-z0-9_]+)|(^custom$)',
'replace_pattern' => '[^a-z0-9_()]+', // Added '()' characters from exclusion list since creation state has it.
'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores.'),
],
];
$row['sysid'] = [
'#type' => 'value',
'#value' => $state->sysid,
];
$row['status'] = [
'#type' => 'checkbox',
'#default_value' => $state->isActive(),
];
// The new value of states that are inactivated.
$row['reassign'] = [
'#type' => 'select',
'#options' => $current_state_options,
];
$row['count'] = [
'#type' => 'value',
'#value' => $count,
'#markup' => $count,
];
// Disable some properties for Creation state.
if ($state->isCreationState()) {
$row['status']['#disabled'] = TRUE;
$row['reassign']['#type'] = 'hidden';
$row['reassign']['#disabled'] = TRUE;
}
// New state and disabled states cannot be reassigned.
if (!$sid || !$state->isActive() || ($count == 0) ) {
$row['reassign']['#type'] = 'hidden';
$row['reassign']['#disabled'] = TRUE;
}
// Disabled states cannot be renamed (and is a visual clue, too).
if (!$state->isActive()) {
$row['label_new']['#disabled'] = TRUE;
}
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// Get the Workflow from the page.
/** @var $workflow \Drupal\workflow\Entity\Workflow */
if (!$workflow = workflow_url_get_workflow()) {
return $form;
}
$wid = $workflow->getWorkflowId();
$form = parent::buildForm($form, $form_state);
// Add a sticky header.
$form[$this->entitiesKey] += [
'#sticky' => TRUE,
];
// Build select options for reassigning states.
// We put a blank state first for validation.
$state_options = workflow_get_workflow_state_names($wid, FALSE);
// Is this the last state available?
$form['#last_mohican'] = (count($state_options) == 1);
// Create a placeholder WorkflowState (It must NOT be saved to DB). Add it to the item list.
$sid = '';
$placeholder = $workflow->createState($sid, FALSE);
$placeholder->set('label', '');
$this->entities['placeholder'] = $placeholder;
$form['entities']['placeholder'] = $this->buildRow($placeholder);
// Rename 'submit' button.
$form['actions']['submit']['#value'] = t('Save');
return $form;
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
/** @var $state \Drupal\workflow\Entity\WorkflowState */
$state = $entity;
// $workflow = $state->getWorkflow();
/**
* Allow modules to insert their own workflow operations to the list.
*/
// This is what EntityListBuilder::getOperations() does:
// $operations = $this->getDefaultOperations($entity);
// $operations += $this->moduleHandler()->invokeAll('entity_operation', [$entity]);
// $this->moduleHandler->alter('entity_operation', $operations, $entity);
// In D8, the interface of below hook_workflow_operations has changed a bit.
// @see EntityListBuilder::getOperations, workflow_operations, workflow.api.php.
$operations += $this->moduleHandler()->invokeAll('workflow_operations', ['state', $state]);
return $operations;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->getValue($this->entitiesKey) as $sid => $value) {
if (isset($this->entities[$sid])) {
/** @var $state WorkflowState */
$state = $this->entities[$sid];
// Does user want to deactivate the state (reassign current content)?
if ($sid && $value['status'] == 0 && $state->isActive()) {
workflow_debug( __FILE__, __FUNCTION__, __LINE__, '', ''); // @todo D8-port: still test this snippet.
$args = ['%state' => $state->label()];
// Does that state have content in it?
if ($value['count'] > 0 && empty($value['reassign'])) {
if ($form['#last_mohican']) {
$message = 'Since you are deleting the last available
workflow state in this workflow, all content items
which are in that state will have their workflow state
removed.';
drupal_set_message($this->t($message, $args), 'warning');
}
else {
$message = 'The %state state has content; you must
reassign the content to another state.';
$form_state->setErrorByName("states'][$sid]['reassign'", $this->t($message, $args));
}
}
}
// Create the machine_name for new states.
// N.B.: Keep machine_name in WorkflowState and ~ListBuilder aligned.
if ($value['label_new'] && !$value['id']) {
//$message = 'Machine name is required.';
//$form_state->setErrorByName('machine_name', $this->t($message));
}
}
}
return;
}
/**
* {@inheritdoc}
*
* Overrides DraggableListBuilder::submitForm().
* The WorkflowState entities are always saved.
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// parent::submitForm($form, $form_state);
// Get the Workflow from the page.
/** @var $workflow \Drupal\workflow\Entity\Workflow */
if (!$workflow = workflow_url_get_workflow()) {
return ;
}
// The default min_weight is -10. Work with it.
$creation_weight = -11;
$max_weight = -9;
foreach ($form_state->getValue($this->entitiesKey) as $sid => $value) {
if (isset($this->entities[$sid])) {
/** @var $state WorkflowState */
$state = $this->entities[$sid];
// Is the new state name empty?
if (empty($value['label_new'])) {
// No new state entered, so skip it.
continue;
}
// Does user want to deactivate the state (reassign current content)?
if ($sid && $value['status'] == 0 && $state->isActive()) {
$new_sid = $value['reassign'];
$new_state = WorkflowState::load($new_sid);
$args = [
'%workflow' => $workflow->label(),
'%old_state' => $state->label(),
'%new_state' => isset($new_state) ? $new_state->label() : '',
];
if ($value['count'] > 0) {
if ($form['#last_mohican']) {
$new_sid = NULL; // Do not reassign to new state.
$message = 'Removing workflow states from content in the %workflow.';
drupal_set_message($this->t($message, $args));
}
else {
// Prepare the state delete function.
$message = 'Reassigning content from %old_state to %new_state.';
drupal_set_message($this->t($message, $args));
}
}
// Delete the old state without orphaning content, move them to the new state.
$state->deactivate($new_sid);
$message = $this->t('Deactivated workflow state %old_state in %workflow.', $args);
\Drupal::logger('workflow')->notice($message, []);
drupal_set_message($message);
}
// Set a proper weight to the new state.
$max_weight = max($max_weight, $state->get($this->weightKey));
// Is this a new state?
if ($sid == 'placeholder' && empty(!$value['label_new'])) {
// New state, add it.
$state->set('id', $value['id']);
// Set a proper weight to the new state.
$state->set($this->weightKey, $max_weight + 1);
}
elseif ($value['sysid'] == WORKFLOW_CREATION_STATE) {
// Set a proper weight to the creation state.
$state->set($this->weightKey, $creation_weight);
}
else {
$state->set($this->weightKey, $value['weight']);
}
$state->set('label', $value['label_new']);
$state->set('status', $value['status']);
$state->save();
}
}
drupal_set_message(t('The Workflow states have been updated.'));
return;
}
/**
* Validate duplicate machine names. Function registered in 'machine_name'
* form element.
*
* @param string $name
* @param array $element
* @param \Drupal\Core\Form\FormStateInterface $form_state
*
* @return bool
*/
function exists($name, array $element, FormStateInterface $form_state) {
$state_names = [];
foreach ($form_state->getValue($this->entitiesKey) as $sid => $value) {
$state_names[] = $value['id'];
}
$state_names = array_map('strtolower', $state_names);
$result = array_unique(array_diff_assoc($state_names, array_unique($state_names)));
if (in_array($name, $result)) {
return TRUE;
}
return FALSE;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\workflow_ui\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Returns responses for Workflow UI routes.
*/
class WorkflowUiController extends ControllerBase {
/**
* Returns the settings page.
*
* @return array
* Renderable array.
*/
public function settingsForm() {
$element = [
'#markup' => 'Workflow settings form is not implemented yet.',
];
return $element;
}
}
@@ -0,0 +1,191 @@
<?php
namespace Drupal\workflow_ui\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflow\Entity\WorkflowState;
/**
* Defines a class to build a draggable listing of Workflow Config Transitions entities.
*
* @see \Drupal\workflow\Entity\WorkflowConfigTransition
*/
abstract class WorkflowConfigTransitionFormBase extends ConfigFormBase {
/**
* The key to use for the form element containing the entities.
*
* @var string
*/
protected $entitiesKey = 'entities';
/**
* The WorkflowConfigTransition form type.
*
* @var string
*/
protected $type;
/**
* The entities being listed.
*
* @var \Drupal\Core\Entity\EntityInterface[]
*/
protected $entities = [];
/**
* The workflow object.
*
* @var \Drupal\workflow\Entity\Workflow
*/
protected $workflow;
/**
* {@inheritdoc}
*/
public function __construct() {
// The $this->type and $this->entitiesKey must be set in the var section.
// Get the Workflow from the page.
$this->workflow = workflow_url_get_workflow();
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workflow_config_transition_' . $this->type . '_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [];
}
/**
* {@inheritdoc}
*
* Create an $entity for every ConfigTransition.
*/
public function load() {
$entities = [];
$entity_type = $this->entitiesKey;
$workflow = $this->workflow;
$states = $workflow->getStates($all = 'CREATION');
if ($states) {
/* @var $from_state WorkflowState */
/* @var $to_state WorkflowState */
switch ($entity_type) {
case 'workflow_state':
foreach ($states as $from_state) {
$from_sid = $from_state->id();
$entities[$from_sid] = $from_state;
}
break;
case 'workflow_config_transition':
foreach ($states as $from_state) {
$from_sid = $from_state->id();
foreach ($states as $to_state) {
$to_sid = $to_state->id();
// Don't allow transition TO (creation).
if ($to_state->isCreationState()) {
continue;
}
// // Only allow transitions from $from_state.
// if ($state->id() <> $from_state->id()) {
// continue;
// }
// Load existing config_transitions. Create if not found.
$config_transitions = $workflow->getTransitionsByStateId($from_sid, $to_sid);
if (!$config_transition = reset($config_transitions)) {
$config_transition = $workflow->createTransition($from_sid, $to_sid);
}
$entities[] = $config_transition;
}
}
break;
default:
drupal_set_message(t('Improper type provided in load method.'), 'error');
\Drupal::logger('workflow_ui')->notice('Improper type provided in load method.', []);
return $entities;
}
}
return $entities;
}
/**
* {@inheritdoc}
*/
// abstract public function buildHeader();
/**
* {@inheritdoc}
*/
// abstract public function buildRow(EntityInterface $entity);
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = [];
if (!$this->workflow) {
return $form;
}
/*
* Begin of copied code DraggableListBuilder::buildForm()
*/
$form[$this->entitiesKey] = [
'#type' => 'table',
'#header' => $this->buildHeader(),
'#sticky' => TRUE,
'#empty' => t('There is no @label yet.', ['@label' => 'Transition']),
'#tabledrag' => [['action' => 'order', 'relationship' => 'sibling', 'group' => 'weight', ], ],
];
$this->entities = $this->load();
$delta = 10;
// Change the delta of the weight field if have more than 20 entities.
if (!empty($this->weightKey)) {
$count = count($this->entities);
if ($count > 20) {
$delta = ceil($count / 2);
}
}
foreach ($this->entities as $entity) {
$row = $this->buildRow($entity);
if (isset($row['label'])) {
$row['label'] = ['#markup' => $row['label']];
}
if (isset($row['weight'])) {
$row['weight']['#delta'] = $delta;
}
$form[$this->entitiesKey][$entity->id()] = $row;
}
/*
* End of copied code DraggableListBuilder::buildForm()
*/
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
}
}
@@ -0,0 +1,103 @@
<?php
namespace Drupal\workflow_ui\Form;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines a class to build a draggable listing of Workflow Config Transitions entities.
*
* @see \Drupal\workflow\Entity\WorkflowConfigTransition
*/
class WorkflowConfigTransitionLabelForm extends WorkflowConfigTransitionFormBase {
/**
* {@inheritdoc}
*/
protected $entitiesKey = 'workflow_config_transition';
/**
* {@inheritdoc}
*/
protected $type = 'label';
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header = [
'from' => t('Transition from'),
'to' => t('Transition to'),
'label_new' => t('label'),
'config_transition' => '',
];
return $header;
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row = [];
$workflow = $this->workflow;
if ($workflow) {
/* @var $entity \Drupal\workflow\Entity\WorkflowConfigTransition */
$config_transition = $entity;
static $previous_from_sid = -1;
// Get transitions, sorted by weight of the old state.
$from_state = $config_transition->getFromState();
$to_state = $config_transition->getToState();
$from_sid = $from_state->id();
// Skip the transitions without any roles.
$skip = TRUE;
foreach ($config_transition->roles as $rid => $active) {
if ($active) {
$skip = FALSE;
}
}
if ($skip == TRUE && ($from_state != $to_state)) {
return $row;
}
$row['from'] = [
'#type' => 'value',
'#markup' => ($previous_from_sid != $from_sid) ? $from_state->label() : '"',
];
$row['to'] = [
'#type' => 'value',
'#markup' => $to_state->label(),
];
$row['label_new'] = [
'#type' => 'textfield',
'#default_value' => $config_transition->get('label'),
];
$row['config_transition'] = [
'#type' => 'value',
'#value' => $config_transition,
];
$previous_from_sid = $from_sid;
}
return $row;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->getValue($this->entitiesKey) as $key => $value) {
$new_label = trim($value['label_new']);
$value['config_transition']
->set('label', $new_label)
->save();
}
drupal_set_message(t('The transition labels have been saved.'));
}
}
@@ -0,0 +1,181 @@
<?php
namespace Drupal\workflow_ui\Form;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflow\Entity\WorkflowConfigTransition;
use Drupal\workflow\Entity\WorkflowState;
/**
* Defines a class to build a listing of Workflow Config Transitions entities.
*
* @see \Drupal\workflow\Entity\WorkflowConfigTransition
*/
class WorkflowConfigTransitionRoleForm extends WorkflowConfigTransitionFormBase {
/**
* {@inheritdoc}
*/
protected $entitiesKey = 'workflow_state';
/**
* {@inheritdoc}
*/
protected $type = 'permission';
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header = [];
$workflow = $this->workflow;
$states = $workflow->getStates($all = 'CREATION');
if ($states) {
$header['label_new'] = t('From \ To');
/* @var $state WorkflowState */
foreach ($states as $state) {
// Don't allow transition TO (creation).
if (!$state->isCreationState()) {
$header[$state->id()] = t('@label', ['@label' => $state->label()]);
}
}
}
return $header;
}
/**
* {@inheritdoc}
*
* Builds a row for the following table:
* Transitions, for example:
* 18 => [
* 20 => [
* 'author' => 1,
* 1 => 0,
* 2 => 1,
* ]
* ]
* means the transition from state 18 to state 20 can be executed by
* the content author or a user in role 2. The $transitions array should
* contain ALL transitions for the workflow.
*/
public function buildRow(EntityInterface $entity) {
$row = [];
$workflow = $this->workflow;
if ($workflow) {
// Each $entity is a from-state.
/* @var $entity \Drupal\workflow\Entity\WorkflowState */
$from_state = $entity;
$from_sid = $from_state->id();
/* @var $states WorkflowState[] */
$states = $workflow->getStates($all = 'CREATION');
if ($states) {
// Only get the roles with proper permission + Author role.
$type_id = $workflow->id();
$roles = workflow_get_user_role_names("create $type_id workflow_transition");
// Prepare default value for 'stay_on_this_state'.
$allow_all_roles = []; // array_combine (array_keys($roles) , array_keys($roles));
/* @var $state WorkflowState */
foreach ($states as $state) {
$row['to'] = [
'#type' => 'value',
'#markup' => t('@label', ['@label' => $from_state->label()]),
];
/* @var $to_state WorkflowState */
foreach ($states as $to_state) {
// Don't allow transition TO (creation).
if ($to_state->isCreationState()) {
continue;
}
// Only allow transitions from $from_state.
if ($state->id() <> $from_state->id()) {
continue;
}
$to_sid = $to_state->id();
$stay_on_this_state = ($to_sid == $from_sid);
// Load existing config_transitions. Create if not found.
$config_transitions = $workflow->getTransitionsByStateId($from_sid, $to_sid);
if (!$config_transition = reset($config_transitions)) {
$config_transition = $workflow->createTransition($from_sid, $to_sid);
}
$row[$to_sid]['workflow_config_transition'] = ['#type' => 'value', '#value' => $config_transition, ];
$row[$to_sid]['roles'] = [
'#type' => $stay_on_this_state ? 'checkboxes' : 'checkboxes',
'#options' => $stay_on_this_state ? [] : $roles,
'#disabled' => $stay_on_this_state,
// When $stay_on_this_state, allow all roles.
'#default_value' => $stay_on_this_state ? $allow_all_roles : $config_transition->roles,
];
}
}
}
}
return $row;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$workflow = $this->workflow;
// Make sure 'author' is checked for (creation) -> [something].
$creation_state = $workflow->getCreationState();
if (empty($form_state->getValue($this->entitiesKey))) {
$author_has_permission = TRUE;
}
else {
$author_has_permission = FALSE;
foreach ($form_state->getValue($this->entitiesKey) as $from_sid => $to_data) {
foreach ($to_data as $to_sid => $transition_data) {
if ($from_sid == $to_sid) {
// Same-state-transition do not count.
}
elseif (!empty($transition_data['roles'][WORKFLOW_ROLE_AUTHOR_RID])) {
$author_has_permission = TRUE;
break;
}
}
}
}
if (!$author_has_permission) {
$form_state->setErrorByName('id', t('Please give the author permission to go from %creation to at least one state!',
['%creation' => $creation_state->label()]));
}
return;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($form_state->getValue($this->entitiesKey) as $from_sid => $to_data) {
foreach ($to_data as $transition_data) {
/* @var $config_transition WorkflowConfigTransition */
if (isset($transition_data['workflow_config_transition'])) {
$config_transition = $transition_data['workflow_config_transition'];
$config_transition->roles = $transition_data['roles'];
$config_transition->save();
}
else {
// Should not be possible.
// $config_transition = [];
}
}
}
drupal_set_message(t('The workflow was updated.'));
}
}
@@ -0,0 +1,17 @@
name: 'Workflow UI'
description: 'Administrative UI for Workflow.'
package: Workflow
type: module
# core: 8.x
# version: VERSION
configure: entity.workflow_type.collection
dependencies:
- workflow
# Information added by Drupal.org packaging script on 2017-12-17
version: '8.x-1.0'
core: '8.x'
project: 'workflow'
datestamp: 1513552400
@@ -0,0 +1,24 @@
<?php
/**
* @file
* Install, update and uninstall functions for the workflow_ui module.
*/
/**
* Implements hook_install().
*/
function workflow_ui_install() {
$url = \Drupal\Core\Url::fromRoute('user.admin_permissions', [],
['fragment' => 'module-workflow']);
$message = t("Please review which roles may 'participate in workflows'
<a href=':url'>on the Permissions page</a>.",
[':url' => $url->toString()]);
drupal_set_message($message);
}
/**
* Drupal 8 updates.
*/
function workflow_ui_update_8001(&$sandbox) {
}
@@ -0,0 +1,13 @@
# Action links for Workflow UI.
### Workflow related actions.
workflow_type_add_action:
route_name: entity.workflow_type.add_form
title: 'Add workflow'
weight: 1
appears_on:
- entity.workflow_type.collection
### Workflow State related actions.
### Workflow Transition related actions.
@@ -0,0 +1,7 @@
# Menu links for Workflow UI.
workflow.ui.workflows:
title: 'Workflow'
route_name: entity.workflow_type.collection
parent: system.admin_config_workflow
description: 'Workflow administration overview.'
@@ -0,0 +1,28 @@
# Menu tasks for Workflow UI.
workflow.ui.settings:
title: 'Settings'
route_name: workflow.ui.settings
base_route: entity.workflow_type.collection
weight: 9
workflow.ui.workflows:
title: 'Workflows'
route_name: entity.workflow_type.collection
base_route: entity.workflow_type.collection
weight: 1
workflow.ui.workflow_states:
title: 'States'
route_name: entity.workflow_state.collection
base_route: entity.workflow_type.edit_form
weight: -4
workflow.ui.workflow_transitions:
title: 'Transitions'
route_name: entity.workflow_transition.collection
base_route: entity.workflow_type.edit_form
weight: -3
workflow.ui.workflow_labels:
title: 'Transition labels'
route_name: entity.workflow_label.collection
base_route: entity.workflow_type.edit_form
weight: -2
@@ -0,0 +1,93 @@
<?php
/**
* @file
* Provides administrative UI for workflow.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function workflow_ui_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'entity.workflow_transition.field_ui_fields':
return t('This page allows you to add fields to the Workflow form.
Normally this is an advanced action, which is not needed in
regular use cases.');
case 'entity.workflow_type.collection':
return t('This page allows you to maintain Workflows. Once a workflow is
created, you can maintain your entity type and add a Field of type
\'Workflow\'.');
case 'entity.workflow_type.add_form':
return '';
case 'entity.workflow_state.collection':
return t("To create a new state, enter its name in the last row of the
'State' column. Check the 'Active' box to make it effective. You may
also drag it to the appropriate position.") . '<br />'
. t("A state must be marked as active, to be available in the
workflow's transitions.") . '<br />'
. t("If you wish to inactivate a state that has content (i.e. count is
not zero), then you need to select a state to which to reassign that
content.");
case 'entity.workflow_transition.collection':
$url = \Drupal\Core\Url::fromRoute('user.admin_permissions', [],
['fragment' => 'module-workflow']);
return t('You are currently viewing the possible transitions to and from
workflow states. The state is shown in the left column; the state to be
moved to is to the right. For each transition, check the box next to
the role(s) that may initiate the transition. For example, if only the
"production editor" role may move content from Review state to the
Published state, check the box next to "production editor". The author
role is built in and refers to the user who authored the content.')
. '<br /><i>'
. t("If not all roles are in the list, please review which roles may
'participate in workflows' on <a href=':url'>the Permissions page</a>.
On that page, uncheck the 'Authenticated user' role temporarily to
view the permissions of each separate role.</i>",
[':url' => $url->toString()]);
case 'entity.workflow_label.collection':
return t('You can add labels to transitions if you don\'t like the
standard state labels. They will modify the Workflow form options, so
specific workflow transitions can have their own labels, relative to
the beginning and ending states. Rather than showing the user a
workflow box containing options like "review required" as a state in
the workflow, it could say "move to the editing department for grammar
review".');
}
return [];
}
/**
* Helper function to determine Workflow from Workflow UI URL.
*
* @deprecated : @see workflow_url_get_workflow .
*/
function workflow_ui_url_get_workflow($url = '' ) {
return workflow_url_get_workflow($url);
}
/**
* Helper function to determine the title of the page.
*
* @deprecated : @see workflow_url_get_title .
*/
function workflow_ui_url_get_title() {
return workflow_url_get_title();
}
/**
* Helper function to determine Workflow from Workflow UI URL.
*
* @deprecated : @see workflow_url_get_form_type .
*/
function workflow_ui_url_get_form_type($url = '' ) {
return workflow_url_get_form_type($url);
}
@@ -0,0 +1,46 @@
# Declaration of Workflow UI routings.
# copy from WorkflowUIController.php
# https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/group/entity_api/8
### Workflow settings form
workflow.ui.settings:
path: '/admin/config/workflow/workflow/settings'
defaults:
_controller: '\Drupal\workflow_ui\Controller\WorkflowUiController::settingsForm'
_title: 'Workflow settings'
requirements:
_permission: 'administer workflow'
### Workflow States
entity.workflow_state.collection:
path: '/admin/config/workflow/workflow/{workflow_type}/states'
defaults:
_entity_list: 'workflow_state'
# _controller: '\workflow\EntityWorkflowUIController::adminOverview'
_title: 'Edit Workflow'
_title_callback: 'workflow_url_get_title'
requirements:
_permission: 'administer workflow'
### Workflow Transitions
entity.workflow_transition.collection:
path: '/admin/config/workflow/workflow/{workflow_type}/transition_roles'
defaults:
_form: 'Drupal\workflow_ui\Form\WorkflowConfigTransitionRoleForm'
_title: 'Edit Workflow'
_title_callback: 'workflow_url_get_title'
requirements:
_permission: 'administer workflow'
### Workflow Labels
entity.workflow_label.collection:
path: '/admin/config/workflow/workflow/{workflow_type}/transition_labels'
defaults:
_form: 'Drupal\workflow_ui\Form\WorkflowConfigTransitionLabelForm'
_title: 'Edit Workflow'
_title_callback: 'workflow_url_get_title'
requirements:
_permission: 'administer workflow'
@@ -0,0 +1,217 @@
<?php
namespace Drupal\workflow\Controller;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\Controller\EntityListController;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\views\Views;
use Drupal\workflow\Entity\WorkflowManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for Workflow routes.
*/
class WorkflowTransitionListController extends EntityListController implements ContainerInjectionInterface {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs an object.
*
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(DateFormatter $date_formatter, RendererInterface $renderer) {
// These parameters are taken from some random other controller.
$this->dateFormatter = $date_formatter;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('date.formatter'),
$container->get('renderer')
);
}
/**
* Generates an overview table of older revisions of a node,
* but only if this::historyAccess() allows it.
*
* @param EntityInterface $node
* A node object.
* @return array An array as expected by drupal_render().
* An array as expected by drupal_render().
*/
public function historyOverview(EntityInterface $node = NULL) {
$form = [];
/*
* Get data from parameters.
*/
// @todo D8-port: make Workflow History tab happen for every entity_type.
// For workflow_tab_page with multiple workflows, use a separate view. See [#2217291].
// @see workflow.routing.yml, workflow.links.task.yml, WorkflowTransitionListController.
// workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
// ATM it only works for Nodes and Terms.
// This is a hack. The Route should always pass an object.
// On view tab, $entity is object,
// On workflow tab, $entity is id().
// Get the entity for this form.
if (!$entity = workflow_url_get_entity($node)) {
return $form;
}
/*
* Get derived data from parameters.
*/
if (!$field_name = workflow_get_field_name($entity, workflow_url_get_field_name())) {
return $form;
}
/*
* Step 1: generate the Transition Form.
*/
// Add the WorkflowTransitionForm to the page.
$form = WorkflowManager::getWorkflowTransitionForm($entity, $field_name);
/*
* Step 2: generate the Transition History List.
*/
$view = NULL;
$moduleHandler = \Drupal::service('module_handler');
if ($moduleHandler->moduleExists('views')){
$view = Views::getView('workflow_entity_history');
}
if (is_object($view) && $view->storage->status()) {
// Add the history list from configured Views display.
$args = [$entity->id()];
$view->setArguments($args);
$view->setDisplay('workflow_history_tab');
$view->preExecute();
$view->execute();
$form['table'] = $view->buildRenderable();
}
else {
// @deprecated. Use the Views display above.
// Add the history list from programmed WorkflowTransitionListController.
$entity_type = 'workflow_transition';
// $form = $this->listing('workflow_transition');
$list_builder = $this->entityTypeManager()->getListBuilder($entity_type);
// Add the Node explicitly, since $list_builder expects a Transition.
$list_builder->workflow_entity = $entity;
$form += $list_builder->render();
}
/*
* Finally: sort the elements (overriding their weight).
*/
// $form['#weight'] = 10;
$form['actions']['#weight'] = 100;
$form['table']['#weight'] = 201;
return $form;
}
/**
* Menu access control callback. Checks access to Workflow tab.
*
* This used to be D7-function workflow_tab_access($user, $entity).
*
* The History tab should not be used with multiple workflows per entity.
* Use the dedicated view for this use case.
* @todo D8: remove this in favour of View 'Workflow history per entity'.
* @todo D8-port: make this workflow for non-Node entity types.
*
* @param \Drupal\Core\Session\AccountInterface $account
* Run access checks for this account.
*
* @return \Drupal\Core\Access\AccessResult
*/
public function historyAccess(AccountInterface $account) {
static $access = [];
$uid = ($account) ? $account->id() : -1;
// @todo D8-port: make Workflow History tab happen for every entity_type.
// @see workflow.routing.yml, workflow.links.task.yml, WorkflowTransitionListController.
// ATM it only works for Nodes and Terms.
// This is a hack. The Route should always pass an object.
// On view tab, $entity is object,
// On workflow tab, $entity is id().
// Get the entity for this form.
$entity = workflow_url_get_entity();
/* @var $entity EntityInterface */
// Figure out the $entity's bundle and id.
$entity_type = $entity->getEntityTypeId();
$entity_bundle = $entity->bundle();
$entity_id = ($entity) ? $entity->id() : '';
$field_name = workflow_url_get_field_name();
if (isset($access[$uid][$entity_type][$entity_id][$field_name ? $field_name : 'no_field'])) {
return $access[$uid][$entity_type][$entity_id][$field_name ? $field_name : 'no_field'];
}
$access_result = AccessResult::forbidden();
// When having multiple workflows per bundle, use Views display
// 'Workflow history per entity' instead!
$fields = _workflow_info_fields($entity, $entity_type, $entity_bundle, $field_name);
if (!$fields) {
return AccessResult::forbidden();
}
else {
// @todo: Keep below code aligned between WorkflowState, ~Transition, ~TransitionListController
$uid = ($account) ? $account->id() : -1;
$entity_id = ($entity) ? $entity->id() : '';
// Determine if user is owner of the entity.
$is_owner = WorkflowManager::isOwner($account, $entity);
/**
* Determine if user has Access. Fill the cache.
*/
// @todo: what to do with multiple workflow_fields per bundle? Use Views instead! Or introduce a setting.
// @todo D8-port: workflow_tab_access: use proper 'WORKFLOW_TYPE' permissions
foreach ($fields as $definition) {
$type_id = $definition->getSetting('workflow_type');
if ($account->hasPermission("access any $type_id workflow_transion overview")) {
$access_result = AccessResult::allowed();
}
elseif ($is_owner && $account->hasPermission("access own $type_id workflow_transion overview")) {
$access_result = AccessResult::allowed();
}
elseif ($account->hasPermission('administer nodes')) {
$access_result = AccessResult::allowed();
}
$access[$uid][$entity_type][$entity_id][$field_name ? $field_name : 'no_field'] = $access_result;
}
}
return $access_result;
}
}
@@ -0,0 +1,587 @@
<?php
namespace Drupal\workflow\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
use Drupal\workflow\Entity\WorkflowManager;
use Drupal\workflow\Entity\WorkflowScheduledTransition;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
/**
* Provides a form element for the WorkflowTransitionForm and ~Widget.
*
* Properties:
* - #return_value: The value to return when the checkbox is checked.
*
* @see \Drupal\Core\Render\Element\FormElement
* @see https://www.drupal.org/node/169815 "Creating Custom Elements"
*
* @FormElement("workflow_transition")
*/
class WorkflowTransitionElement extends FormElement {
/**
* Form element validation handler.
*
* Note that #maxlength is validated by _form_validate() already.
*
* This checks that the submitted value:
* - Does not contain the replacement character only.
* - Does not contain disallowed characters.
* - Is unique; i.e., does not already exist.
* - Does not exceed the maximum length (via #maxlength).
* - Cannot be changed after creation (via #disabled).
*
* @param array $element Reference to the Form element
* @param \Drupal\Core\Form\FormStateInterface $form_state
* @param array $complete_form
*/
public static function validateTransition(&$element, FormStateInterface $form_state, &$complete_form) {
workflow_debug( __FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
}
/**
* Generate an element.
*
* This function is referenced in the Annotation for this class.
*
* @param $element
* @param FormStateInterface $form_state
* @param $complete_form
*
* @return array
* The Workflow element
*/
public static function processTransition(&$element, FormStateInterface $form_state, &$complete_form) {
workflow_debug( __FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
return self::transitionElement($element, $form_state, $complete_form);
}
/**
* Generate an element.
*
* This function is an internal function, to be reused in:
* - TransitionElement
* - TransitionDefaultWidget
*
* Usage:
* @example $element['#default_value'] = $transition;
* @example $element += WorkflowTransitionElement::transitionElement($element, $form_state, $form);
*
* @param array $element Reference to the form element
* @param FormStateInterface $form_state
* @param array $complete_form
*
* @return array
* The form element
*/
public static function transitionElement(&$element, FormStateInterface $form_state, &$complete_form) {
// $element = [];
/*
* Input.
*/
// A Transition object must have been set explicitly.
/** @var $transition WorkflowTransitionInterface */
$transition = $element['#default_value'];
/** @var $user \Drupal\Core\Session\AccountInterface */
$user = \Drupal::currentUser();
/*
* Derived input.
*/
$field_name = $transition->getFieldName();
$workflow = $transition->getWorkflow();
$wid = $transition->getWorkflowId();
$force = $transition->isForced();
$entity = $transition->getTargetEntity();
$entity_type = $transition->getTargetEntityTypeId();
$entity_id = $transition->getTargetEntityId();
if ($transition->isExecuted()) {
// We are editing an existing/executed/not-scheduled transition.
// Only the comments may be changed!
$current_sid = $from_sid = $transition->getFromSid();
// The states may not be changed anymore.
$to_state = $transition->getToState();
$options = [$to_state->id() => $to_state->label()];
// We need the widget to edit the comment.
$show_widget = TRUE;
$default_value = $transition->getToSid();
}
elseif ($entity) {
// Normal situation: adding a new transition on an new/existing entity.
// Get the scheduling info, only when updating an existing entity.
// This may change the $default_value on the Form.
// Technically you could have more than one scheduled transition, but
// this will only add the soonest one.
// @todo: Read the history with an explicit langcode?
$langcode = ''; // $entity->language()->getId();
if ($entity_id && $scheduled_transition = WorkflowScheduledTransition::loadByProperties($entity_type, $entity_id, [], $field_name, $langcode)) {
$transition = $scheduled_transition;
}
$current_sid = $from_sid = $transition->getFromSid();
$current_state = $from_state = $transition->getFromState();
$options = ($current_state) ? $current_state->getOptions($entity, $field_name, $user, FALSE) : [];
$show_widget = ($from_state) ? $from_state->showWidget($entity, $field_name, $user, FALSE) : [];
$default_value = $from_sid;
$default_value = ($from_state && $from_state->isCreationState()) ? $workflow->getFirstSid($entity, $field_name, $user, FALSE) : $default_value;
$default_value = ($transition->isScheduled()) ? $transition->getToSid() : $default_value;
}
elseif (!$entity) {
// Sometimes, no entity is given. We encountered the following cases:
// - D7: the Field settings page,
// - D7: the VBO action form;
// - D7/D8: the Advance Action form on admin/config/system/actions;
// If so, show all options for the given workflow(s).
if (!$temp_state = $transition->getFromState()) {
$temp_state = $transition->getToState();
}
$options = ($temp_state)
? $temp_state->getOptions($entity, $field_name, $user, FALSE)
: workflow_get_workflow_state_names($wid, $grouped = TRUE);
$show_widget = TRUE;
$current_sid = $transition->getToSid(); // @todo
$default_value = $from_sid = $transition->getToSid(); // @todo
}
else {
// We are in trouble! A message is already set in workflow_node_current_state().
$options = [];
$current_sid = 0;
$show_widget = FALSE;
$default_value = FALSE;
}
// Fetch the form ID. This is unique for each entity, to allow multiple form per page (Views, etc.).
// Make it uniquer by adding the field name, or else the scheduling of
// multiple workflow_fields is not independent of each other.
// If we are indeed on a Transition form (so, not a Node Form with widget)
// then change the form id, too.
$form_id = self::getFormId();
/*
* Output: generate the element.
*/
// Get settings from workflow. @todo: implement default_settings.
if ($workflow) {
$workflow_settings = $workflow->options;
}
else {
// @todo D8-port: now only tested with Action.
$workflow_settings = [
'name_as_title' => 0,
'options' => "radios",
'schedule_timezone' => 1,
'comment_log_node' => "1",
'watchdog_log' => TRUE,
];
}
// Current sid and default value may differ in a scheduled transition.
$workflow_settings['comment'] = $workflow_settings['comment_log_node']; // 'comment_log_tab' is removed;
// Capture settings to format the form/widget.
$settings_title_as_name = !empty($workflow_settings['name_as_title']);
$settings_fieldset = isset($workflow_settings['fieldset']) ? $workflow_settings['fieldset'] : 0;
$settings_options_type = $workflow_settings['options'];
// Display scheduling form if user has permission.
// Not shown on new entity (not supported by workflow module, because that
// leaves the entity in the (creation) state until scheduling time.)
// Not shown when editing existing transition.
$type_id = ($workflow) ? $workflow->id() : ''; // Might be empty on Action configuration.
$settings_schedule = !$transition->isExecuted() && $user->hasPermission("schedule $type_id workflow_transition");
if ($settings_schedule) {
//@todo D8-port: check below code: form on VBO.
// workflow_debug( __FILE__ , __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
$step = $form_state->getValue('step');
if (isset($step) && ($form_state->getValue('step') == 'views_bulk_operations_config_form')) {
// On VBO 'modify entity values' form, leave field settings.
$settings_schedule = TRUE;
}
else {
// ... and cannot be shown on a Content add page (no $entity_id),
// ...but can be shown on a VBO 'set workflow state to..'page (no entity).
$settings_schedule = !($entity && !$entity_id);
}
}
$settings_schedule_timezone = !empty($workflow_settings['schedule_timezone']);
// Show comment, when both Field and Instance allow this.
$settings_comment = $workflow_settings['comment'];
$transition_is_scheduled = $transition->isScheduled();
// Save the current value of the entity in the form, for later Workflow-module specific references.
// We add prefix, since #tree == FALSE.
$element['workflow_transition'] = [
'#type' => 'value',
'#value' => $transition,
];
// Decide if we show a widget or a formatter.
// There is no need for a widget when the only option is the current sid.
// Add a state formatter before the rest of the form,
// when transition is scheduled or widget is hidden.
if ( (!$show_widget) || $transition_is_scheduled || $transition->isExecuted()) {
$element['workflow_current_state'] = workflow_state_formatter($entity, $field_name, $current_sid);
// Set a proper weight, which works for Workflow Options in select list AND action buttons.
$element['workflow_current_state']['#weight'] = -0.005;
}
$element['#tree'] = TRUE;
// Add class following node-form pattern (both on form and container).
$workflow_type_id = ($workflow) ? $workflow->id() : '';
$element['#attributes']['class'][] = 'workflow-transition-' . $workflow_type_id . '-container';
$element['#attributes']['class'][] = 'workflow-transition-container';
if (!$show_widget) {
// Show no widget.
$element['to_sid']['#type'] = 'value';
$element['to_sid']['#value'] = $default_value;
$element['to_sid']['#options'] = $options; // In case action buttons need them.
$element['comment']['#type'] = 'value';
$element['comment']['#value'] = '';
return $element; // <-- exit.
}
// @todo: repair the usage of $settings_title_as_name: no container if no details (schedule/comment).
// Prepare a UI wrapper. This might be a fieldset.
if ($settings_fieldset == 0) { // Use 'container'.
$element += [
'#type' => 'container',
];
}
else {
$element += [
'#type' => 'details',
'#collapsible' => TRUE,
'#open' => ($settings_fieldset == 2) ? FALSE : TRUE,
];
}
$element['field_name'] = [
'#type' => 'select',
'#title' => t('Field name'),
'#description' => t('Choose the field name.'),
'#access' => FALSE, // Only show on VBO/Actions screen.
'#options' => workflow_get_workflow_field_names($entity),
'#default_value' => $field_name,
'#required' => TRUE,
'#weight' => -20,
];
$element['force'] = [
'#type' => 'checkbox',
'#title' => t('Force transition'),
'#description' => t('If this box is checked, the new state will be assigned even if workflow permissions disallow it.'),
'#access' => FALSE, // Only show on VBO/Actions screen.
'#default_value' => $force,
'#weight' => -19,
];
// This overrides BaseFieldDefinition. @todo: apply for form and widget.
// The 'options' widget. May be removed later if 'Action buttons' are chosen.
// The help text is not available for container. Let's add it to the
// State box. N.B. it is empty on Workflow Tab, Node View page.
$help_text = isset($element['#description']) ? $element['#description'] : '';
// This overrides BaseFieldDefinition. @todo: apply for form and widget.
$element['to_sid'] = [
'#type' => ($wid) ? $settings_options_type : 'select', // Avoid error with grouped options.
'#title' => ($settings_title_as_name && !$transition->isExecuted())
? t('Change @name state', ['@name' => $workflow->label()])
: t('Target state'),
'#access' => TRUE,
'#options' => $options,
// '#parents' => array('workflow'),
'#default_value' => $default_value,
'#description' => $help_text,
];
// Display scheduling form under certain conditions.
if ($settings_schedule == TRUE) {
$timezone = $user->getTimeZone();
$timezone_options = array_combine(timezone_identifiers_list(), timezone_identifiers_list());
$timestamp = $transition ? $transition->getTimestamp() : \Drupal::time()->getRequestTime();
$hours = (!$transition_is_scheduled) ? '00:00' : \Drupal::service('date.formatter')->format($timestamp, 'custom', 'H:i', $timezone);
// Add a container, so checkbox and time stay together in extra fields.
$element['workflow_scheduling'] = [
'#type' => 'container',
'#tree' => TRUE,
];
$element['workflow_scheduling']['scheduled'] = [
'#type' => 'radios',
'#title' => t('Schedule'),
'#options' => [
'0' => t('Immediately'),
'1' => t('Schedule for state change'),
],
'#default_value' => $transition_is_scheduled ? '1' : '0',
'#attributes' => [
// 'id' => 'scheduled_' . $form_id,
'class' => [Html::getClass('scheduled_' . $form_id)],
],
];
$element['workflow_scheduling']['date_time'] = [
'#type' => 'details', // 'container',
'#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE.
'#attributes' => ['class' => ['container-inline']],
'#prefix' => '<div style="margin-left: 1em;">',
'#suffix' => '</div>',
'#states' => [
//'visible' => array(':input[id="' . 'scheduled_' . $form_id . '"]' => array('value' => '1')),
'visible' => ['input.' . Html::getClass('scheduled_' . $form_id) => ['value' => '1']],
],
];
$element['workflow_scheduling']['date_time']['workflow_scheduled_date'] = [
'#type' => 'date',
'#prefix' => t('At'),
'#default_value' => implode( '-', [
'year' => date('Y', $timestamp),
'month' => date('m', $timestamp),
'day' => date('d', $timestamp),
]
)
];
$element['workflow_scheduling']['date_time']['workflow_scheduled_hour'] = [
'#type' => 'textfield',
'#title' => t('Time'),
'#maxlength' => 7,
'#size' => 6,
'#default_value' => $hours,
'#element_validate' => ['_workflow_transition_form_element_validate_time'], // @todo D8-port: this is not called.
];
$element['workflow_scheduling']['date_time']['workflow_scheduled_timezone'] = [
'#type' => $settings_schedule_timezone ? 'select' : 'hidden',
'#title' => t('Time zone'),
'#options' => $timezone_options,
'#default_value' => [$timezone => $timezone],
];
$element['workflow_scheduling']['date_time']['workflow_scheduled_help'] = [
'#type' => 'item',
'#prefix' => '<br />',
'#description' => t('Please enter a time.
If no time is included, the default will be midnight on the specified date.
The current time is: @time.', ['@time' => \Drupal::service('date.formatter')->format(\Drupal::time()->getRequestTime(), 'custom', 'H:i', $timezone)]
),
];
}
// This overrides BaseFieldDefinition. @todo: apply for form and widget.
$element['comment'] = [
'#type' => 'textarea',
'#required' => $settings_comment == '2',
'#access' => $settings_comment != '0', // Align with action buttons.
'#title' => t('Workflow comment'),
'#description' => t('A comment to put in the workflow log.'),
'#default_value' => $transition ? $transition->getComment() : '',
'#rows' => 2,
];
// In WorkflowTransitionForm, a default 'Submit' button is added over there.
// In Entity Form, a button per permitted state is added in workflow_form_alter().
if ($settings_options_type == 'buttons' || $settings_options_type == 'dropbutton') {
// D7: How do action buttons work? See also d.o. issue #2187151.
// D7: Create 'action buttons' per state option. Set $sid property on each button.
// 1. Admin sets ['widget']['options']['#type'] = 'buttons'.
// 2. This function formElement() creates 'action buttons' per state option;
// sets $sid property on each button.
// 3. User clicks button.
// 4. Callback _workflow_transition_form_validate_buttons() sets proper State.
// 5. Callback _workflow_transition_form_validate_buttons() sets Submit function.
// Performance: inform workflow_form_alter() to do its job.
_workflow_use_action_buttons($settings_options_type);
// Make sure the '#type' is not set to the invalid 'buttons' value.
// It will be replaced by action buttons, but sometimes, the select box
// is still shown.
// @see workflow_form_alter().
$element['to_sid']['#type'] = 'select';
$element['to_sid']['#access'] = FALSE;
}
return $element;
}
/**
* Returns a unique string identifying the form.
*
* @return string
*/
protected static function getFormId() {
return 'workflow_transition_form'; //@todo D8-port: add $form_id for widget and History tab.
}
/**
* Implements ContentEntityForm::copyFormValuesToEntity(), and is called from:
* - WorkflowTransitionForm::copyFormValuesToEntity()
* - WorkflowDefaultWidget
*
* N.B. in contrary to ContentEntityForm::copyFormValuesToEntity(),
* - parameter 1 is returned as result, to be able to create a new Transition object.
* - parameter 3 is not $form_state (from Form), but an $item array (from Widget).
*
* @param \Drupal\Core\Entity\EntityInterface $transition
* @param array $form
* @param FormStateInterface $form_state
* @param array $item
*
* @return WorkflowTransitionInterface
*/
static public function copyFormValuesToTransition(EntityInterface $transition, array $form, FormStateInterface $form_state, array $item) {
/** @var WorkflowTransitionInterface $transition */
/** @var \Drupal\user\UserInterface $user */
$user = workflow_current_user(); // @todo #2287057: verify if submit() really is only used for UI. If not, $user must be passed.
/**
* Derived input
*/
// Make sure we have subset ['workflow_scheduled_date_time']
if (!isset($item['to_sid'])) {
$entity_id = $transition->getTargetEntityId();
drupal_set_message(t('Error: content @id has no workflow attached. The data is not saved.', ['@id' => $entity_id]), 'error');
// The new state is still the previous state.
return $transition;
}
// In WorkflowTransitionForm, we receive the complete $form_state.
// Remember, the workflow_scheduled element is not set on 'add' page.
$scheduled = !empty($item['workflow_scheduling']['scheduled']);
$schedule_values = ($scheduled) ? $item['workflow_scheduling']['date_time'] : [];
// Get user input from element.
$to_sid = $item['to_sid'];
$comment = $item['comment'];
$force = FALSE;
// @todo D8: add the VBO use case.
/*
// Determine if the transition is forced.
// This can be set by a 'workflow_vbo action' in an additional form element.
$force = isset($form_state['input']['workflow_force']) ? $form_state['input']['workflow_force'] : FALSE;
if (!$entity) {
// E.g., on VBO form.
}
*/
// @todo D8-port: add below exception.
// Extract the data from $items, depending on the type of widget.
// @todo D8: use MassageFormValues($item, $form, $form_state).
/*
$old_sid = workflow_node_previous_state($entity, $entity_type, $field_name);
if (!$old_sid) {
// At this moment, $old_sid should have a value. If the content does not
// have a state yet, old_sid contains '(creation)' state. But if the
// content is not associated to a workflow, old_sid is now 0. This may
// happen in workflow_vbo, if you assign a state to non-relevant nodes.
$entity_id = entity_id($entity_type, $entity);
drupal_set_message(t('Error: content @id has no workflow attached. The data is not saved.', array('@id' => $entity_id)), 'error');
// The new state is still the previous state.
$new_sid = $old_sid;
return $new_sid;
}
*/
$timestamp = \Drupal::time()->getRequestTime();
if ($scheduled) {
// Fetch the (scheduled) timestamp to change the state.
// Override $timestamp.
$scheduled_date_time = implode(' ', [
$schedule_values['workflow_scheduled_date'],
$schedule_values['workflow_scheduled_hour'],
// $schedule_values['workflow_scheduled_timezone'],
]);
$timezone = $schedule_values['workflow_scheduled_timezone'];
$old_timezone = date_default_timezone_get();
date_default_timezone_set($timezone);
$timestamp = strtotime($scheduled_date_time);
date_default_timezone_set($old_timezone);
if (!$timestamp) {
// Time should have been validated in form/widget.
$timestamp = \Drupal::time()->getRequestTime();
}
}
/**
* Process
*/
/*
* Create a new ScheduledTransition.
*/
if ($scheduled) {
$transition_entity = $transition->getTargetEntity();
$field_name = $transition->getFieldName();
$from_sid = $transition->getFromSid();
/** @var $transition WorkflowTransitionInterface */
$transition = WorkflowScheduledTransition::create([$from_sid, 'field_name' => $field_name]);
$transition->setTargetEntity($transition_entity);
$transition->setValues($to_sid, $user->id(), $timestamp, $comment);
}
if (!$transition->isExecuted()) {
// Set new values.
// When editing an existing Transition, only comments may change.
$transition->setValues($to_sid, $user->id(), $timestamp, $comment);
$transition->schedule($scheduled);
$transition->force($force);
}
$transition->setComment($comment);
// Determine and add the attached fields.
// Caveat: This works automatically on a Workflow Form,
// but only with a hack on a widget.
// @todo: Attached fields are not supported in ScheduledTransitions.
$fields = WorkflowManager::getAttachedFields('workflow_transition', $transition->bundle());
/** @var \Drupal\Core\Field\Entity\BaseFieldOverride $nodeField */
foreach ($fields as $field_name => $field) {
$user_input = isset($form_state->getUserInput()[$field_name]) ? $form_state->getUserInput()[$field_name] : [];
if (isset($item[$field_name])) {
// On Workflow Form (e.g., history tab, block).
// @todo, in latest tests, this line seems not necessary.
$transition->{$field_name} = $item[$field_name];
}
elseif ($user_input) {
// On Workflow Widget (e.g., on node, comment).
// @todo, some field types are not supported here.
$transition->{$field_name} = $user_input;
}
}
return $transition;
}
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#return_value' => 1,
'#process' => [
[$class, 'processTransition'],
[$class, 'processAjaxForm'],
//array($class, 'processGroup'),
],
'#element_validate' => [
[$class, 'validateTransition'],
],
'#pre_render' => [
[$class, 'preRenderTransition'],
//array($class, 'preRenderGroup'),
],
//'#theme' => 'input__checkbox',
//'#theme' => 'input__textfield',
'#theme_wrappers' => ['form_element'],
//'#title_display' => 'after',
];
}
}
@@ -0,0 +1,444 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Workflow configuration entity to persistently store configuration.
*
* @ConfigEntityType(
* id = "workflow_type",
* label = @Translation("Workflow type"),
* label_singular = @Translation("Workflow type"),
* label_plural = @Translation("Workflow types"),
* label_count = @PluralTranslation(
* singular = "@count Workflow type",
* plural = "@count Workflow types",
* ),
* module = "workflow",
* static_cache = TRUE,
* translatable = TRUE,
* handlers = {
* "storage" = "Drupal\workflow\Entity\WorkflowStorage",
* "list_builder" = "Drupal\workflow_ui\Controller\WorkflowListBuilder",
* "form" = {
* "add" = "Drupal\workflow\Form\WorkflowTypeForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* "edit" = "Drupal\workflow\Form\WorkflowTypeForm",
* }
* },
* admin_permission = "administer workflow",
* config_prefix = "workflow",
* bundle_of = "workflow_transition",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* },
* config_export = {
* "id",
* "label",
* "module",
* "status",
* "options",
* },
* links = {
* "canonical" = "/admin/config/workflow/workflow/{workflow_type}",
* "collection" = "/admin/config/workflow/workflow",
* "delete-form" = "/admin/config/workflow/workflow/{workflow_type}/delete",
* "edit-form" = "/admin/config/workflow/workflow/{workflow_type}",
* },
* )
*/
class Workflow extends ConfigEntityBase implements WorkflowInterface {
/**
* The machine name.
*
* @var string
*/
public $id;
/**
* The human readable name.
*
* @var string
*/
public $label;
// TODO D8-port Workflow: complete below variables. (Add get()-functions).
// @see https://www.drupal.org/node/1809494
// @see https://codedrop.com.au/blog/creating-custom-config-entities-drupal-8
public $options = [];
/**
* The workflow-specific creation state.
*/
private $creation_state;
private $creation_sid = 0;
// Attached States and Transitions.
public $states = [];
public $transitions = [];
/**
* CRUD functions.
*/
/**
* Given information, update or insert a new workflow.
*
* This also handles importing, rebuilding, reverting from Features,
* as defined in workflow.features.inc.
* TODO D8: clean up this function, since we are config entity now.
* todo D7: reverting does not refresh States and transitions, since no
* machine_name was present. As of 7.x-2.3, the machine_name exists in
* Workflow and WorkflowConfigTransition, so rebuilding is possible.
*
* When changing this function, test with the following situations:
* - maintain Workflow in Admin UI;
* - clone Workflow in Admin UI;
* - create/revert/rebuild Workflow with Features; @see workflow.features.inc
* - save Workflow programmatic;
*
* @inheritdoc
*/
public function save() {
$status = parent::save();
// Are we saving a new Workflow?
// Make sure a Creation state exists.
if ($status == SAVED_NEW) {
$this->getCreationState();
}
return $status;
}
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageInterface $storage, array &$entities) {
/** @var Workflow $workflow */
foreach($entities as &$workflow) {
//Better performance, together with Annotation static_cache = TRUE.
// Load the states, and set the creation state.
$workflow->getStates();
$workflow->getCreationState();
}
}
/**
* Given a wid, delete the workflow and its data.
*/
public function delete() {
if (!$this->isDeletable()) {
// @todo: throw error if not workflow->isDeletable().
}
else {
// Delete associated state (also deletes any associated transitions).
foreach ($this->getStates($all = TRUE) as $state) {
$state->deactivate('');
$state->delete();
}
// Delete the workflow.
parent::delete();
}
}
/**
* {@inheritdoc}
*/
public function isValid() {
$is_valid = TRUE;
// Don't allow Workflow without states. There should always be a creation state.
$states = $this->getStates($all = FALSE);
if (count($states) < 1) {
// That's all, so let's remind them to create some states.
$message = t('Workflow %workflow has no states defined, so it cannot be assigned to content yet.',
['%workflow' => $this->label()]);
drupal_set_message($message, 'warning');
// Skip allowing this workflow.
$is_valid = FALSE;
}
// Also check for transitions, at least out of the creation state. Don't filter for roles.
$transitions = $this->getTransitionsByStateId($this->getCreationSid(), '');
if (count($transitions) < 1) {
// That's all, so let's remind them to create some transitions.
$message = t('Workflow %workflow has no transitions defined, so it cannot be assigned to content yet.',
['%workflow' => $this->label()]);
drupal_set_message($message, 'warning');
// Skip allowing this workflow.
$is_valid = FALSE;
}
return $is_valid;
}
/**
* {@inheritdoc}
*/
public function isDeletable() {
// May not be deleted if assigned to a Field.
foreach ($fields = _workflow_info_fields() as $field_info) {
if ($field_info->getSetting('workflow_type') == $this->id()) {
return FALSE;
}
}
return TRUE;
}
/**
* Property functions.
*/
/**
* {@inheritdoc}
*/
public function getWorkflowId() {
return $this->id();
}
/**
* {@inheritdoc}
*/
public function createState($sid, $save = TRUE) {
$wid = $this->id();
/* @var $state WorkflowState */
$state = WorkflowState::load($sid);
if (!$state || $wid != $state->getWorkflowId()) {
$state = WorkflowState::create($values = ['id' => $sid, 'wid' => $wid]);
if ($save) {
$state->save();
}
}
// Maintain the new object in the workflow.
$this->states[$state->id()] = $state;
return $state;
}
/**
* {@inheritdoc}
*/
public function getCreationState() {
// First, find it.
if (!$this->creation_state) {
foreach ($this->getStates($all = TRUE) as $state) {
if ($state->isCreationState()) {
$this->creation_state = $state;
$this->creation_sid = $state->id();
}
}
}
// First, then, create it.
if (!$this->creation_state) {
$state = $this->createState(WORKFLOW_CREATION_STATE_NAME);
$this->creation_state = $state;
$this->creation_sid = $state->id();
}
return $this->creation_state;
}
/**
* {@inheritdoc}
*/
public function getCreationSid() {
if (!$this->creation_sid) {
$state = $this->getCreationState();
return $state->id();
}
return $this->creation_sid;
}
/**
* {@inheritdoc}
*/
public function getFirstSid(EntityInterface $entity, $field_name, AccountInterface $user, $force = FALSE) {
$creation_state = $this->getCreationState();
$options = $creation_state->getOptions($entity, $field_name, $user, $force);
if ($options) {
$keys = array_keys($options);
$sid = $keys[0];
}
else {
// This should never happen, but it did during testing.
drupal_set_message(t('There are no workflow states available. Please notify your site administrator.'), 'error');
$sid = 0;
}
return $sid;
}
/**
* {@inheritdoc}
*/
public function getNextSid(EntityInterface $entity, $field_name, AccountInterface $user, $force = FALSE) {
$current_sid = WorkflowManager::getCurrentStateId($entity, $field_name);
/* @var $current_state WorkflowState */
$current_state = WorkflowState::load($current_sid);
$options = $current_state->getOptions($entity, $field_name, $user, $force);
// Loop over every option. To find the next one.
$flag = $current_state->isCreationState();
$new_sid = $current_state->id();
foreach ($options as $sid => $name) {
if ($flag) {
$new_sid = $sid;
break;
}
if ($sid == $current_state->id()) {
$flag = TRUE;
}
}
return $new_sid;
}
/**
* {@inheritdoc}
*/
public function getStates($all = FALSE, $reset = FALSE) {
$wid = $this->id();
if ($reset) {
$this->states = $wid ? WorkflowState::loadMultiple([], $wid, $reset) : [];
}
elseif ($this->states === NULL) {
$this->states = $wid ? WorkflowState::loadMultiple([], $wid, $reset) : [];
}
elseif ($this->states === []) {
$this->states = $wid ? WorkflowState::loadMultiple([], $wid, $reset) : [];
}
// Do not unset, but add to array - you'll remove global objects otherwise.
$states = [];
foreach ($this->states as $state) {
$id = $state->id();
if ($all === TRUE) {
$states[$id] = $state;
}
elseif (($all === FALSE) && ($state->isActive() && !$state->isCreationState())) {
$states[$id] = $state;
}
elseif (($all == 'CREATION') && ($state->isActive() || $state->isCreationState())) {
$states[$id] = $state;
}
else {
// Do not add state.
}
}
return $states;
}
/**
* {@inheritdoc}
*/
public function getState($sid) {
$wid = $this->id();
$state = WorkflowState::load($sid);
if (!$wid || $wid == $state->getWorkflowId()) {
return $state;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function createTransition($from_sid, $to_sid, $values = []) {
$config_transition = NULL;
// First check if this transition already exists.
$transitions = $this->getTransitionsByStateId($from_sid, $to_sid);
if ($transitions) {
$config_transition = reset($transitions);
}
else {
$values['wid'] = $this->id();
$values['from_sid'] = $from_sid;
$values['to_sid'] = $to_sid;
$config_transition = WorkflowConfigTransition::create($values);
$config_transition->save();
}
// Maintain the new object in the workflow.
$this->transitions[$config_transition->id()] = $config_transition;
return $config_transition;
}
/**
* {@inheritdoc}
*/
public function sortTransitions() {
// Sort the transitions on state weight.
uasort($this->transitions, ['Drupal\workflow\Entity\WorkflowConfigTransition', 'sort'] );
}
/**
* {@inheritdoc}
*/
public function getTransitions(array $ids = NULL, array $conditions = []) {
$config_transitions = [];
// Get filters on 'from' states, 'to' states, roles.
$from_sid = isset($conditions['from_sid']) ? $conditions['from_sid'] : FALSE;
$to_sid = isset($conditions['to_sid']) ? $conditions['to_sid'] : FALSE;
// Get valid states + creation state.
$states = $this->getStates('CREATION');
// Cache all transitions in the workflow.
if (!$this->transitions) {
$this->transitions = WorkflowConfigTransition::loadMultiple($ids);
$this->sortTransitions();
}
/* @var $config_transition WorkflowConfigTransition */
foreach ($this->transitions as &$config_transition) {
if (!isset($states[$config_transition->getFromSid()])) {
// Not a valid transition for this workflow. @todo: delete them.
}
elseif ($from_sid && $from_sid != $config_transition->getFromSid()) {
// Not the requested 'from' state.
}
elseif ($to_sid && $to_sid != $config_transition->getToSid()) {
// Not the requested 'to' state.
}
else {
// Transition is allowed, permitted. Add to list.
$config_transitions[$config_transition->id()] = $config_transition;
}
}
return $config_transitions;
}
/**
* {@inheritdoc}
*/
public function getTransitionsById($tid) {
return $this->getTransitions([$tid]);
}
/**
* {@inheritdoc}
*/
public function getTransitionsByStateId($from_sid, $to_sid) {
$conditions = [
'from_sid' => $from_sid,
'to_sid' => $to_sid,
];
return $this->getTransitions(NULL, $conditions);
}
}
@@ -0,0 +1,222 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\user\UserInterface;
/**
* Workflow configuration entity to persistently store configuration.
*
* @ConfigEntityType(
* id = "workflow_config_transition",
* label = @Translation("Workflow config transition"),
* label_singular = @Translation("Workflow config transition"),
* label_plural = @Translation("Workflow config transitions"),
* label_count = @PluralTranslation(
* singular = "@count Workflow config transition",
* plural = "@count Workflow config transitions",
* ),
* module = "workflow",
* translatable = FALSE,
* handlers = {
* "form" = {
* "delete" = "\Drupal\Core\Entity\EntityDeleteForm",
* }
* },
* admin_permission = "administer workflow",
* config_prefix = "transition",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* },
* config_export = {
* "id",
* "label",
* "module",
* "from_sid",
* "to_sid",
* "roles",
* },
* links = {
* "collection" = "/admin/config/workflow/workflow/{workflow_type}/transitions",
* },
* )
*/
class WorkflowConfigTransition extends ConfigEntityBase implements WorkflowConfigTransitionInterface {
// Transition data.
public $id;
public $from_sid;
public $to_sid;
public $roles = [];
// Extra fields.
protected $wid;
// The following must explicitly defined, and not be public, to avoid errors
// when exporting with json_encode().
protected $workflow = NULL;
/*
* Entity class functions.
*/
/**
* {@inheritdoc}
*/
public function __construct(array $values = [], $entityType = NULL) {
// Please be aware that $entity_type and $entityType are different things!
return parent::__construct($values, $entity_type = 'workflow_config_transition');
}
/**
* Helper function for __construct. Used for all children of WorkflowTransition (aka WorkflowScheduledTransition)
* @param $from_sid
* @param $to_sid
*/
public function setValues($from_sid, $to_sid) {
$this->from_sid = $from_sid;
$this->to_sid = $to_sid;
}
/**
* {@inheritdoc}
*/
public function save() {
$workflow = $this->getWorkflow();
// To avoid double posting, check if this (new) transition already exist.
if (empty($this->id())) {
if ($workflow) {
$config_transitions = $workflow->getTransitionsByStateId($this->from_sid, $this->to_sid);
$config_transition = reset($config_transitions);
if ($config_transition) {
$this->set('id', $config_transition->id());
}
}
}
// Create the machine_name. This can be used to rebuild/revert the Feature in a target system.
if (empty($this->id())) {
$wid = $workflow->id();
$this->set('id', implode('', [$wid, substr($this->from_sid, strlen($wid)), substr($this->to_sid, strlen($wid))]));
}
$status = parent::save();
if ($status) {
// Save in current workflow for the remainder of this page request.
// Keep in sync with Workflow::getTransitions() !
if ($workflow) {
$workflow->transitions[$this->id()] = $this;
// $workflow->sortTransitions();
}
}
return $status;
}
/** @noinspection PhpMissingParentCallCommonInspection
* {@inheritdoc}
*/
public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) {
// Sort the entities using the entity class's sort() method.
// See \Drupal\Core\Config\Entity\ConfigEntityBase::sort().
/** @var WorkflowTransitionInterface $a */
/** @var WorkflowTransitionInterface $b */
if (!$a->getFromSid() || !$b->getFromSid()) {
return 0;
}
// First sort on From-State.
$from_state_a = $a->getFromState();
$from_state_b = $b->getFromState();
if ($from_state_a->weight < $from_state_b->weight) return -1;
if ($from_state_a->weight > $from_state_b->weight) return +1;
// Then sort on To-State.
$to_state_a = $a->getToState();
$to_state_b = $b->getToState();
if ($to_state_a->weight < $to_state_b->weight) return -1;
if ($to_state_a->weight > $to_state_b->weight) return +1;
return 0;
}
/**
* Property functions.
*/
/**
* {@inheritdoc}
*/
public function getWorkflow() {
if (!$this->workflow && $wid = $this->getWorkflowId()) {
$this->workflow = Workflow::load($wid);
}
return $this->workflow;
}
/**
* {@inheritdoc}
*/
public function getWorkflowId() {
if (!$this->wid) {
$from_sid = $this->getFromSid();
$to_sid = $this->getToSid();
$state = WorkflowState::load($to_sid ? $to_sid : $from_sid);
$this->wid = $state->getWorkflowId();
}
return $this->wid;
}
/**
* {@inheritdoc}
*/
public function getFromState() {
return WorkflowState::load($this->from_sid);
}
/**
* {@inheritdoc}
*/
public function getToState() {
return WorkflowState::load($this->to_sid);
}
/**
* {@inheritdoc}
*/
public function getFromSid() {
return $this->from_sid;
}
/**
* {@inheritdoc}
*/
public function getToSid() {
return $this->to_sid;
}
/**
* {@inheritdoc}
*/
public function isAllowed(UserInterface $user, $force = FALSE) {
$type_id = $this->getWorkflowId();
if ($user->hasPermission("bypass $type_id workflow_transition access")) {
// Superuser is special. And $force allows Rules to cause transition.
return TRUE;
}
if ($force) {
return TRUE;
}
if ($this->getFromSid() == $this->getToSid()) {
// Anyone may save an entity without changing state.
return TRUE;
}
return TRUE == array_intersect($user->getRoles(), $this->roles);
}
}
@@ -0,0 +1,69 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\user\UserInterface;
/**
* Defines a common interface for Workflow*Transition* objects.
*
* @see \Drupal\workflow\Entity\WorkflowConfigTransition
* @see \Drupal\workflow\Entity\WorkflowTransition
* @see \Drupal\workflow\Entity\WorkflowScheduledTransition
*/
interface WorkflowConfigTransitionInterface {
/**
* Determines if the current transition between 2 states is allowed:
* - in settings;
* - in permissions;
* - by permission hooks, implemented by other modules.
*
* @param \Drupal\user\UserInterface $user
* The user to act upon.
* May have the custom WORKFLOW_ROLE_AUTHOR_RID role.
* @param bool $force
* Indicates if the transition must be forced(E.g., by cron, rules).
*
* @return bool
* TRUE if OK, else FALSE.
*/
public function isAllowed(UserInterface $user, $force = FALSE);
/**
* Returns the Workflow object of this State.
*
* @return Workflow
* Workflow object.
*/
public function getWorkflow();
/**
* Returns the Workflow ID of this Transition
*
* @return string
* Workflow Id.
*/
public function getWorkflowId();
/**
* @return WorkflowState
*/
public function getFromState();
/**
* @return WorkflowState
*/
public function getToState();
/**
* @return string
*/
public function getFromSid();
/**
* @return string
*/
public function getToSid();
}
@@ -0,0 +1,169 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines a common interface for Workflow*Transition* objects.
*
* @see \Drupal\workflow\Entity\WorkflowConfigTransition
* @see \Drupal\workflow\Entity\WorkflowTransition
* @see \Drupal\workflow\Entity\WorkflowScheduledTransition
*/
interface WorkflowInterface {
/**
* Returns the workflow id.
*
* @return string
* $wid
*/
public function getWorkflowId();
/**
* Validate the workflow. Generate a message if not correct.
*
* This function is used on the settings page of:
* - Workflow field: WorkflowItem->settingsForm()
*
* @return bool
* $is_valid
*/
public function isValid();
/**
* Returns if the Workflow may be deleted.
*
* @return bool
* TRUE if a Workflow may safely be deleted.
*/
public function isDeletable();
/**
* Create a new state for this workflow.
*
* @param string $sid
* @param bool $save
* Indicator if the new state must be saved. Normally, the new State is
* saved directly in the database. This is because you can use States only
* with Transitions, and they rely on State IDs which are generated
* magically when saving the State. But you may need a temporary state.
* @return \Drupal\workflow\Entity\WorkflowState
* The new state.
*/
public function createState($sid, $save = TRUE);
/**
* Gets the initial state for a newly created entity.
*/
public function getCreationState();
/**
* Gets the ID of the initial state for a newly created entity.
*/
public function getCreationSid();
/**
* Gets the first valid state ID, after the creation state.
*
* Uses WorkflowState::getOptions(), because this does an access check.
* The first State ID is user-dependent!
*
* @param EntityInterface|null $entity
* The entity at hand. May be NULL (E.g., on a Field settings page).
* @param $field_name
* @param AccountInterface $user
* @param bool $force
*
* @return string
* A State ID.
*/
public function getFirstSid(EntityInterface $entity, $field_name, AccountInterface $user, $force = FALSE);
/**
* Returns the next state for the current state.
* Is used in VBO Bulk actions.
*
* @param EntityInterface $entity
* The entity at hand.
* @param $field_name
* @param AccountInterface $user
* @param bool $force
*
* @return string
* A State ID.
*/
public function getNextSid(EntityInterface $entity, $field_name, AccountInterface $user, $force = FALSE);
/**
* Gets all states for a given workflow.
*
* @param mixed $all
* Indicates to which states to return.
* - TRUE = all, including Creation and Inactive;
* - FALSE = only Active states, not Creation;
* - 'CREATION' = only Active states, including Creation.
* @param bool $reset
*
* @return WorkflowState[]
* An array of WorkflowState objects.
*/
public function getStates($all = FALSE, $reset = FALSE);
/**
* Gets a state for a given workflow.
*
* @param string $sid
* A state ID.
*
* @return WorkflowState
* A WorkflowState object.
*/
public function getState($sid);
/**
* Creates a Transition for this workflow.
*
* @param string $from_sid
* @param string $to_sid
* @param array $values
*
* @return mixed|null|static
*/
public function createTransition($from_sid, $to_sid, $values = []);
/**
* Sorts all Transitions for this workflow, according to State weight.
*
* This is only needed for the Admin UI.
*/
public function sortTransitions();
/**
* Loads all allowed ConfigTransitions for this workflow.
*
* @param array|null $ids
* Array of Transitions IDs. If NULL, show all transitions.
* @param array $conditions
* $conditions['from_sid'] : if provided, a 'from' State ID.
* $conditions['to_sid'] : if provided, a 'to' state ID.
*
* @return \Drupal\workflow\Entity\WorkflowConfigTransition[]
*/
public function getTransitions(array $ids = NULL, array $conditions = []);
public function getTransitionsById($tid);
/**
* Get a specific transition.
*
* @param string $from_sid
* @param string $to_sid
*
* @return WorkflowConfigTransition[]
*/
public function getTransitionsByStateId($from_sid, $to_sid);
}
@@ -0,0 +1,468 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\field\Entity\FieldConfig;
/**
* Manages entity type plugin definitions.
*/
class WorkflowManager implements WorkflowManagerInterface {
use StringTranslationTrait;
/**
* The entity_type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity query factory.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $queryFactory;
/**
* The user settings config object.
*
* @var \Drupal\Core\Config\Config
*/
protected $userConfig;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Construct the WorkflowManager object as a service.
* @see workflow.services.yml
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity_type manager service.
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
* The entity query factory.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory, ConfigFactoryInterface $config_factory, TranslationInterface $string_translation, ModuleHandlerInterface $module_handler, AccountInterface $current_user) {
$this->entityTypeManager = $entity_type_manager;
$this->queryFactory = $query_factory;
$this->userConfig = $config_factory->get('user.settings');
$this->stringTranslation = $string_translation;
$this->moduleHandler = $module_handler;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function executeScheduledTransitionsBetween($start = 0, $end = 0) {
$clear_cache = FALSE;
// If the time now is greater than the time to execute a transition, do it.
foreach (WorkflowScheduledTransition::loadBetween($start, $end) as $scheduled_transition) {
$field_name = $scheduled_transition->getFieldName();
$entity = $scheduled_transition->getTargetEntity();
$from_sid = $scheduled_transition->getFromSid();
// Make sure transition is still valid: the entity must still be in
// the state it was in, when the transition was scheduled.
if (!$entity) {
continue;
}
$current_sid = workflow_node_current_state($entity, $field_name);
if (!$current_sid || ($current_sid != $from_sid)) {
// Entity is not in the same state it was when the transition
// was scheduled. Defer to the entity's current state and
// abandon the scheduled transition.
$message = t('Scheduled Transition is discarded, since Entity has state ID %sid1, instead of expected ID %sid2.');
$scheduled_transition->logError($message, 'error', $current_sid, $from_sid);
$scheduled_transition->delete();
continue;
}
// If user didn't give a comment, create one.
$comment = $scheduled_transition->getComment();
if (empty($comment)) {
$scheduled_transition->addDefaultComment();
}
// Do transition. Force it because user who scheduled was checked.
// The scheduled transition is not scheduled anymore, and is also deleted from DB.
// A watchdog message is created with the result.
$scheduled_transition->schedule(FALSE);
$scheduled_transition->executeAndUpdateEntity(TRUE);
if (!$field_name) {
$clear_cache = TRUE;
}
}
if ($clear_cache) {
// Clear the cache so that if the transition resulted in a entity
// being published, the anonymous user can see it.
Cache::invalidateTags(['rendered']);
}
}
/**
* {@inheritdoc}
*/
public static function executeTransitionsOfEntity(EntityInterface $entity) {
// Avoid this hook on workflow objects.
if (in_array($entity->getEntityTypeId(), [
'workflow_type',
'workflow_state',
'workflow_config_transition',
'workflow_transition',
'workflow_scheduled_transition',
])) {
return;
}
$user = workflow_current_user();
foreach (_workflow_info_fields($entity) as $field_info) {
$field_name = $field_info->getName();
// Transition is created in widget or WorkflowTransitionForm.
/** @var $transition WorkflowTransitionInterface */
$transition = $entity->$field_name->__get('workflow_transition');
if (!$transition) {
// We come from creating/editing an entity via entity_form, with core widget or hidden Workflow widget.
// @todo D8: from an Edit form with hidden widget.
/** @noinspection PhpUndefinedFieldInspection */
if ($entity->original) {
// Editing a Node with hidden Widget. State change not possible, so bail out.
// $entity->$field_name->value = $entity->original->$field_name->value;
// continue;
}
// Creating a Node with hidden Workflow Widget. Generate valid first transition.
// @todo D8: Test Creating a non-Node Entity with hidden Workflow widget.
$comment = '';
$old_sid = WorkflowManager::getPreviousStateId($entity, $field_name);
$new_sid = $entity->$field_name->value;
if ((!$new_sid) && $wid = $field_info->getSetting('workflow_type')) {
/** @var Workflow $workflow */
$workflow = Workflow::load($wid);
$new_sid = $workflow->getFirstSid($entity, $field_name, $user);
}
$transition = WorkflowTransition::create([$old_sid, 'field_name' => $field_name]);
$transition->setValues($new_sid, $user->id(), \Drupal::time()->getRequestTime(), $comment, TRUE);
}
// We come from Content/Comment edit page, from widget.
// Set the just-saved entity explicitly. Not necessary for update,
// but upon insert, the old version didn't have an ID, yet.
$transition->setTargetEntity($entity);
if ($transition->isScheduled()) {
$executed = $transition->save(); // Returns a positive integer.
}
elseif ($entity->getEntityTypeId() == 'comment') {
// If Transition is added via CommentForm, save Comment AND Entity.
// Execute and check the result.
$new_sid = $transition->executeAndUpdateEntity();
$executed = ($new_sid == $transition->getToSid()) ? TRUE : FALSE;
}
else {
// Execute and check the result.
$new_sid = $transition->execute();
$executed = ($new_sid == $transition->getToSid()) ? TRUE : FALSE;
}
// If the transition failed, revert the entity workflow status.
// For new entities, we do nothing: it has no original.
if (!$executed && isset($entity->original)) {
$originalValue = $entity->original->{$field_name}->value;
$entity->{$field_name}->setValue($originalValue);
}
}
}
/**
* {@inheritdoc}
*/
public static function getPreviousStateId(EntityInterface $entity, $field_name = '') {
$sid = '';
if (!$entity) {
return $sid;
}
// If $field_name is not known, yet, determine it.
$field_name = ($field_name) ? $field_name : workflow_get_field_name($entity, $field_name);
// If $field_name is found, get more details.
if (!$field_name) {
// Return the initial value.
return $sid;
}
// A node may not have a Workflow attached.
if ($entity->isNew()) {
// A new Node. D7: $is_new is not set when saving terms, etc.
$sid = self::getCreationStateId($entity, $field_name);
}
else {
// @todo?: Read the history with an explicit langcode.
// @todo D8: #2373383 add integration with older revisions via Revisioning module.
$langcode = ''; // $entity->language()->getId();
$entity_type = $entity->getEntityTypeId();
$last_transition = WorkflowTransition::loadByProperties($entity_type, $entity->id(), [], $field_name, $langcode, 'DESC');
if ($last_transition) {
$sid = $last_transition->getToSid(); // @see #2637092, #2612702
}
}
if (!$sid) {
// No history found on an existing entity.
$sid = self::getCreationStateId($entity, $field_name);
}
return $sid;
}
/**
* Gets the creation sid for a given $entity and $field_name.
*
* Is a helper function for:
* - workflow_node_current_state()
* - workflow_node_previous_state()
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* @param string $field_name
*
* @return string
* The ID of the creation State for the Workflow of the field.
*/
private static function getCreationStateId($entity, $field_name) {
$sid = '';
/** @var \Drupal\Core\Config\Entity\ConfigEntityBase $entity */
/** @var \Drupal\Core\Field\FieldDefinitionInterface $field_config */
$field_config = $entity->get($field_name)->getFieldDefinition();
$field_storage = $field_config->getFieldStorageDefinition();
$wid = $field_storage->getSetting('workflow_type');
if ($wid) {
/** @var Workflow $workflow */
$workflow = Workflow::load($wid);
if (!$workflow) {
drupal_set_message(t('Workflow %wid cannot be loaded. Contact your system administrator.', ['%wid' => $wid]), 'error');
}
else {
$sid = $workflow->getCreationSid();
}
}
return $sid;
}
/**
* {@inheritdoc}
*/
public static function deleteUser(AccountInterface $account) {
self::cancelUser([], $account, 'user_cancel_delete');
}
/**
* {@inheritdoc}
*/
public static function cancelUser($edit, AccountInterface $account, $method) {
switch ($method) {
case 'user_cancel_block': // Disable the account and keep its content.
case 'user_cancel_block_unpublish': // Disable the account and unpublish its content.
// Do nothing.
break;
case 'user_cancel_reassign': // Delete the account and make its content belong to the Anonymous user.
case 'user_cancel_delete': // Delete the account and its content.
// Update tables for deleted account, move account to user 0 (anon.)
// ALERT: This may cause previously non-Anonymous posts to suddenly
// be accessible to Anonymous.
/**
* Given a user id, re-assign history to the new user account. Called by user_delete().
*/
$uid = $account->id();
$new_uid = 0;
db_update('workflow_transition_history')
->fields(['uid' => $new_uid])
->condition('uid', $uid, '=')
->execute();
db_update('workflow_transition_schedule')
->fields(['uid' => $new_uid])
->condition('uid', $uid, '=')
->execute();
break;
}
}
/**
* {@inheritdoc}
*/
public static function participateUserRoles(EntityInterface $workflow) {
$type_id = $workflow->id();
foreach (user_roles() as $rid => $role) {
$perms = ["create $type_id workflow_transition" => 1];
user_role_change_permissions($rid, $perms); // <=== Enable Roles.
}
}
/**
* {@inheritdoc}
*/
public static function getWorkflowTransitionForm(EntityInterface $entity, string $field_name) {
// Create a transition, to pass to the form. No need to use setValues().
$current_sid = workflow_node_current_state($entity, $field_name);
$transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]);
$transition->setTargetEntity($entity);
// Create the WorkflowTransitionForm.
/** @var \Drupal\Core\Entity\EntityFormBuilder $entity_form_builder */
$entity_form_builder = \Drupal::getContainer()->get('entity.form_builder');
$form = $entity_form_builder->getForm($transition, 'add');
return $form;
}
/**
* {@inheritdoc}
*/
public function getFields($entity_type_id) {
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
return [];
}
$map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('workflow');
return isset($map[$entity_type_id]) ? $map[$entity_type_id] : [];
}
/**
* {@inheritdoc}
*/
public static function getAttachedFields($entity_type_id, $bundle) {
$added_fields = [];
// Determine the fields added by Field UI.
$entity_field_manager = \Drupal::service('entity_field.manager');
//$extra_fields = $this->entityFieldManager->getExtraFields($entity_type_id, $bundle);
//$base_fields = $entity_field_manager->getBaseFieldDefinitions($entity_type_id, $bundle);
$fields = $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle);
foreach ($fields as $key => $field) {
// Remove BaseFieldDefinition, BaseFieldOverride.
if (($field instanceof FieldConfig)) {
$added_fields[$key] = $field;
}
}
return $added_fields;
}
/**
* {@inheritdoc}
*/
public static function getCurrentStateId(EntityInterface $entity, $field_name = '') {
$sid = '';
if (!$entity) {
return $sid;
}
// If $field_name is not known, yet, determine it.
$field_name = ($field_name) ? $field_name : workflow_get_field_name($entity, $field_name);
// If $field_name is found, get more details.
if (!$field_name || !isset($entity->$field_name)) {
// Return the initial value.
return $sid;
}
// Normal situation: get the value.
$sid = $entity->$field_name->value;
// Entity is new or in preview or there is no current state. Use previous state.
// (E.g., content was created before adding workflow.)
if ( !$sid || !empty($entity->isNew()) || !empty($entity->in_preview) ) {
$sid = self::getPreviousStateId($entity, $field_name);
}
return $sid;
}
/**
* {@inheritdoc}
*/
public static function isOwner(AccountInterface $account, EntityInterface $entity = NULL) {
$is_owner = FALSE;
// @todo: Keep below code aligned between WorkflowState, ~Transition, ~TransitionListController
// Determine if user is owner of the entity.
$uid = ($account) ? $account->id() : -1;
// Get the entity's ID and Author ID.
$entity_id = ($entity) ? $entity->id() : '';
// Some entities (e.g., taxonomy_term) do not have a uid.
// $entity_uid = $entity->get('uid'); // isset($entity->uid) ? $entity->uid : 0;
$entity_uid = (method_exists($entity, 'getOwnerId')) ? $entity->getOwnerId() : -1;
if (!$entity_id) {
// This is a new entity. User is author. Add 'author' role to user.
$is_owner = TRUE;
}
elseif (($entity_uid > 0) && ($uid > 0) && ($entity_uid == $uid)) {
// This is an existing entity. User is author.
// D8: use "access own" permission. D7: Add 'author' role to user.
// N.B.: If 'anonymous' is the author, don't allow access to History Tab,
// since anyone can access it, and it will be published in Search engines.
$is_owner = TRUE;
}
else {
// This is an existing entity. User is not the author. Do nothing.
}
return $is_owner;
}
/**
* {@inheritdoc}
*/
public static function isWorkflowEntityType(string $entity_type_id) {
return in_array($entity_type_id, [
'workflow_type',
'workflow_state',
'workflow_config_transition',
'workflow_transition',
'workflow_scheduled_transition',
]);
}
}
@@ -0,0 +1,173 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides an interface for workflow manager.
*
* Contains lost of functions from D7 workflow.module file.
*/
interface WorkflowManagerInterface {
/**
* Given a time frame, execute all scheduled transitions.
*
* Implements hook_cron().
*
* @param int $start
* @param int $end
*/
public static function executeScheduledTransitionsBetween($start = 0, $end = 0);
/**
* Execute a single transition for the given entity.
*
* Implements hook_entity insert(), hook_entity_update().
*
* When inserting an entity with workflow field, the initial Transition is
* saved without reference to the proper entity, since Id is not yet known.
* So, we cannot save Transition in the Widget, but only(?) in a hook.
* To keep things simple, this is done for both insert() and update().
*
* This is referenced in from WorkflowDefaultWidget::massageFormValues().
* @param \Drupal\Core\Entity\EntityInterface $entity
*/
public static function executeTransitionsOfEntity(EntityInterface $entity);
/********************************************************************
*
* Hook-implementing functions.
*
*/
/**
* Implements hook_WORKFLOW_insert().
*
* Make sure some roles are allowed to participate in a Workflow by default.
*
* @param \Drupal\Core\Entity\EntityInterface $workflow
* @return
*/
public static function participateUserRoles(EntityInterface $workflow);
/**
* Implements hook_user_delete().
*
* @param \Drupal\Core\Session\AccountInterface $account
*/
public static function deleteUser(AccountInterface $account);
/**
* Implements hook_user_cancel().
* Implements deprecated workflow_update_workflow_transition_history_uid().
*
* " When cancelling the account
* " - Disable the account and keep its content.
* " - Disable the account and unpublish its content.
* " - Delete the account and make its content belong to the Anonymous user.
* " - Delete the account and its content.
* "This action cannot be undone.
*
* @param $edit
* @param \Drupal\Core\Session\AccountInterface $account
* @param string $method
*/
public static function cancelUser($edit, AccountInterface $account, $method);
/********************************************************************
*
* Helper functions.
*
*/
/**
* Utility function to return an array of workflow fields.
*
* @param string $entity_type_id
* The content entity type to which the workflow fields are attached.
*
* @return array
* An array of workflow field map definitions, keyed by field name. Each
* value is an array with two entries:
* - type: The field type.
* - bundles: The bundles in which the field appears, as an array with entity
* types as keys and the array of bundle names as values.
*
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldMap()
* @see \Drupal\comment\CommentManagerInterface::getFields()
*/
public function getFields($entity_type_id);
/**
* Gets the TransitionWidget in a form (for e.g., Workflow History Tab)
* @param \Drupal\Core\Entity\EntityInterface $entity
* @param string $field_name
*
* @return
*/
public static function getWorkflowTransitionForm(EntityInterface $entity, string $field_name);
/**
* Returns the attached fields (via Field UI)
*
* @param $entity_type_id
* @param $bundle
*
* @return array
*/
public static function getAttachedFields($entity_type_id, $bundle);
/**
* Gets the current state ID of a given entity.
*
* There is no need to use a page cache.
* The performance is OK, and the cache gives problems when using Rules.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check.
* @param string $field_name
* The name of the field of the entity to check.
* If empty, the field_name is determined on the spot. This must be avoided,
* since it makes having multiple workflow per entity unpredictable.
* The found field_name will be returned in the param.
*
* @return string
* The ID of the current state.
*/
public static function getCurrentStateId(EntityInterface $entity, $field_name = '');
/**
* Gets the previous state ID of a given entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* @param string $field_name
*
* @return string
* The ID of the previous state.
*/
public static function getPreviousStateId(EntityInterface $entity, $field_name = '');
/**
* Determine if User is owner/author of the entity.
*
* @param \Drupal\Core\Session\AccountInterface $account
* @param \Drupal\Core\Entity\EntityInterface $entity
*
* @return bool
*/
public static function isOwner(AccountInterface $account, EntityInterface $entity = NULL);
/**
* Determine if the entity is Workflow* entity type.
* Use it when a function should not operate on Workflow objects.
*
* @param string $entity_type_id
*
* @return bool
*/
public static function isWorkflowEntityType(string $entity_type_id);
}
@@ -0,0 +1,257 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Entity\EntityConstraintViolationList;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Implements a scheduled transition, as shown on Workflow form.
*
* @ContentEntityType(
* id = "workflow_scheduled_transition",
* label = @Translation("Workflow scheduled transition"),
* label_singular = @Translation("Workflow scheduled transition"),
* label_plural = @Translation("Workflow scheduled transitions"),
* label_count = @PluralTranslation(
* singular = "@count Workflow scheduled transition",
* plural = "@count Workflow scheduled transitions",
* ),
* bundle_label = @Translation("Workflow type"),
* module = "workflow",
* handlers = {
* "access" = "Drupal\workflow\WorkflowAccessControlHandler",
* "list_builder" = "Drupal\workflow\WorkflowTransitionListBuilder",
* "form" = {
* "add" = "Drupal\workflow\Form\WorkflowTransitionForm",
* "edit" = "Drupal\workflow\Form\WorkflowTransitionForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* },
* "views_data" = "Drupal\workflow\WorkflowScheduledTransitionViewsData",
* },
* base_table = "workflow_transition_schedule",
* translatable = FALSE,
* entity_keys = {
* "id" = "tid",
* "bundle" = "wid",
* "langcode" = "langcode",
* },
* links = {
* "canonical" = "/workflow_transition/{workflow_transition}",
* "delete-form" = "/workflow_transition/{workflow_transition}/delete",
* "edit-form" = "/workflow_transition/{workflow_transition}/edit",
* },
* )
*/
class WorkflowScheduledTransition extends WorkflowTransition {
/**
* Constructor.
*/
public function __construct(array $values = [], $entityType = 'workflow_scheduled_transition', $bundle = FALSE, $translations = []) {
// Please be aware that $entity_type and $entityType are different things!
parent::__construct($values, $entityType, $bundle, $translations);
// This transition is scheduled.
$this->is_scheduled = TRUE;
// This transition is not executed.
$this->is_executed = FALSE;
}
/**
* {@inheritdoc}
*/
public function setValues($to_sid, $uid = NULL, $scheduled = NULL, $comment = '', $force_create = FALSE) {
parent::setValues($to_sid, $uid, $scheduled, $comment, $force_create);
}
/**
* {@inheritdoc}
*
* This is a hack to avoid the following error, because ScheduledTransition is not a bundle of Workflow:
* Drupal\Component\Plugin\Exception\PluginNotFoundException: The "entity:workflow_scheduled_transition:eerste" plugin does not exist. in Drupal\Core\Plugin\DefaultPluginManager->doGetDefinition() (line 60 of core\lib\Drupal\Component\Plugin\Discovery\DiscoveryTrait.php).
*/
function validate() {
// Since this function generates an error in one use case (using WorkflowTransitionForm)
// and is not called in the other use case (using the Workflow Widget),
// this function is disabled for now.
// @todo: this function is only called in the WorkflowTransitionForm, not in the Widget.
// @todo: repair https://www.drupal.org/node/2896650
// The following is from return parent::validate();
$this->validated = TRUE;
//$violations = $this->getTypedData()->validate();
// return new EntityConstraintViolationList($this, iterator_to_array($violations));
$violations = [];
return new EntityConstraintViolationList($this, $violations);
}
/**
* CRUD functions.
*/
/**
* {@inheritdoc}
*
* Saves a scheduled transition. If the transition is executed, save in history.
*/
public function save() {
// If executed, save in history.
if ($this->is_executed) {
// Be careful, we are not a WorkflowScheduleTransition anymore!
// No fuzzing around, just copy the ScheduledTransition to a normal one.
$current_sid = $this->getFromSid();
$field_name = $this->getFieldName();
$executed_transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]);
$executed_transition->setTargetEntity($this->getTargetEntity());
$executed_transition->setValues($this->getToSid(), $this->getOwnerId(), \Drupal::time()->getRequestTime(), $this->getComment());
return $executed_transition->save(); // <-- exit !!
}
$hid = $this->id();
if (!$hid) {
// Insert the transition. Make sure it hasn't already been inserted.
// @todo: Allow a scheduled transition per revision.
$entity = $this->getTargetEntity();
$found_transition = self::loadByProperties($entity->getEntityTypeId(), $entity->id(), [], $this->getFieldName(), $this->getLangcode());
if ($found_transition) {
// Avoid duplicate entries.
$found_transition->delete();
$result = parent::save();
}
else {
$result = parent::save();
}
}
else {
workflow_debug(__FILE__, __FUNCTION__, __LINE__ ); // @todo D8-port: still test this snippet.
// Update the transition.
$result = parent::save();
}
// Create user message.
if ($state = $this->getToState()) {
$entity = $this->getTargetEntity();
$message = '%entity_title scheduled for state change to %state_name on %scheduled_date';
$args = [
'%entity_title' => $entity->label(),
'%state_name' => $state->label(),
'%scheduled_date' => $this->getTimestampFormatted(),
'link' => ($this->getTargetEntityId() && $this->getTargetEntity()->hasLinkTemplate('canonical')) ? $this->getTargetEntity()->toLink(t('View'))->toString() : '',
];
\Drupal::logger('workflow')->notice($message, $args);
drupal_set_message(t($message, $args));
}
return $result;
}
/**
* {@inheritdoc}
*/
// public static function loadMultiple(array $ids = NULL) {
// return parent::loadMultiple($ids);
// }
/**
* {@inheritdoc}
*/
public static function loadByProperties($entity_type, $entity_id, array $revision_ids = [], $field_name = '', $langcode = '', $sort = 'ASC', $transition_type = 'workflow_scheduled_transition') {
// N.B. $transition_type is set as parameter default.
return parent::loadByProperties($entity_type, $entity_id, $revision_ids, $field_name, $langcode, $sort, $transition_type);
}
/**
* {@inheritdoc}
*/
public static function loadMultipleByProperties($entity_type, array $entity_ids, array $revision_ids = [], $field_name = '', $langcode = '', $limit = NULL, $sort = 'ASC', $transition_type = 'workflow_scheduled_transition') {
// N.B. $transition_type is set as parameter default.
return parent::loadMultipleByProperties($entity_type, $entity_ids, $revision_ids, $field_name, $langcode, $limit, $sort, $transition_type);
}
/**
* Given a time frame, get all scheduled transitions.
*
* @param int $start
* @param int $end
*
* @return WorkflowScheduledTransition[]
* An array of transitions.
*/
public static function loadBetween($start = 0, $end = 0) {
$transition_type = 'workflow_scheduled_transition'; // @todo: get this from annotation.
/* @var $query \Drupal\Core\Entity\Query\QueryInterface */
$query = \Drupal::entityQuery($transition_type)
->sort('timestamp', 'ASC')
->addTag($transition_type);
if ($start) {
$query->condition('timestamp', $start, '>');
}
if ($end) {
$query->condition('timestamp', $end, '<');
}
$ids = $query->execute();
$transitions = self::loadMultiple($ids);
return $transitions;
}
/**
* Property functions.
*/
/**
* If a scheduled transition has no comment, a default comment is added before executing it.
*/
public function addDefaultComment() {
$this->setComment(t('Scheduled by user @uid.', ['@uid' => $this->getOwnerId()]));
}
/**
* Define the fields. Modify the parent fields.
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = [];
// Add the specific ID-field : tid vs. hid.
$fields['tid'] = BaseFieldDefinition::create('integer')
->setLabel(t('Transition ID'))
->setDescription(t('The transition ID.'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
// Get the rest of the fields.
$fields += parent::baseFieldDefinitions($entity_type);
// The timestamp has a different description.
$fields['timestamp'] = []; // Reset old value.
$fields['timestamp'] = BaseFieldDefinition::create('created')
->setLabel(t('Scheduled'))
->setDescription(t('The date+time this transition is scheduled for.'))
->setQueryable(FALSE)
// ->setTranslatable(TRUE)
// ->setDisplayOptions('view', array(
// 'label' => 'hidden',
// 'type' => 'timestamp',
// 'weight' => 0,
// ))
// ->setDisplayOptions('form', array(
// 'type' => 'datetime_timestamp',
// 'weight' => 10,
// ))
// ->setDisplayConfigurable('form', TRUE);
->setRevisionable(TRUE);
// Remove the specific ID-field : tid vs. hid.
unset($fields['hid']);
return $fields;
}
}
@@ -0,0 +1,571 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Language\LanguageInterface;
/**
* Workflow configuration entity to persistently store configuration.
*
* @ConfigEntityType(
* id = "workflow_state",
* label = @Translation("Workflow state"),
* label_singular = @Translation("Workflow state"),
* label_plural = @Translation("Workflow states"),
* label_count = @PluralTranslation(
* singular = "@count Workflow state",
* plural = "@count Workflow states",
* ),
* module = "workflow",
* static_cache = TRUE,
* translatable = FALSE,
* handlers = {
* "access" = "Drupal\workflow\WorkflowAccessControlHandler",
* "list_builder" = "Drupal\workflow_ui\Controller\WorkflowStateListBuilder",
* "form" = {
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* }
* },
* config_prefix = "state",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "weight" = "weight",
* },
* config_export = {
* "id",
* "label",
* "module",
* "wid",
* "weight",
* "sysid",
* "status",
* },
* links = {
* "canonical" = "/admin/config/workflow/workflow/{workflow_type}",
* "collection" = "/admin/config/workflow/workflow/{workflow_type}/states",
* },
* )
*/
class WorkflowState extends ConfigEntityBase {
/**
* The machine name.
*
* @var string
*/
public $id;
/**
* The human readable name.
*
* @var string
*/
public $label;
/**
* The machine_name of the attached Workflow.
*
* @var string
*/
public $wid;
/**
* The weight of this Workflow state.
*
* @var int
*/
public $weight;
/**
* @var int
*/
public $sysid = 0;
public $status = 1;
/**
* The attached Workflow.
*
* @var Workflow
*/
protected $workflow;
/**
* CRUD functions.
*/
/**
* Constructor.
*
* @param array $values
* @param string $entityType
* The name of the new State. If '(creation)', a CreationState is generated.
*/
public function __construct(array $values = [], $entityType = 'workflow_state') {
// Please be aware that $entity_type_id and $entityType are different things!
$sid = isset($values['id']) ? $values['id'] : '';
// Keep official name and external name equal. Both are required.
// @todo: still needed? test import, manual creation, programmatic creation, etc.
if (!isset($values['label']) && $sid) {
$values['label'] = $sid;
}
// Set default values for '(creation)' state.
if ($sid == WORKFLOW_CREATION_STATE_NAME) {
$values['sysid'] = WORKFLOW_CREATION_STATE;
$values['weight'] = WORKFLOW_CREATION_DEFAULT_WEIGHT;
// Do not translate the machine_name.
$values['label'] = t('Creation');
}
parent::__construct($values, $entityType);
}
/**
* {@inheritdoc}
*/
public function save($create_creation_state = TRUE) {
// Create the machine_name for new states.
// N.B.: Keep machine_name in WorkflowState and ~ListBuilder aligned.
$sid = $this->id();
$wid = $this->wid;
$label = $this->label();
// Set the workflow-including machine_name.
if ($sid == WORKFLOW_CREATION_STATE_NAME) {
// Set the initial sid.
$sid = implode('_', [$wid, $sid]);
$this->set('id', $sid);
}
elseif (empty($sid)) {
if ($label) {
$transliteration = \Drupal::service('transliteration');
$value = $transliteration->transliterate($label, LanguageInterface::LANGCODE_DEFAULT, '_');
$value = strtolower($value);
$value = preg_replace('/[^a-z0-9_]+/', '_', $value);
$sid = implode('_', [$wid, $value]);
}
else {
workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
$sid = 'state_' . $this->id();
$sid = implode('_', [$wid, $sid]);
}
$this->set('id', $sid);
}
return parent::save();
}
/**
* Get all states in the system, with options to filter, only where a workflow exists.
*
* @_deprecated WorkflowState::getStates() ==> WorkflowState::loadMultiple()
*
* {@inheritdoc}
*
* @param $wid
* The requested Workflow ID.
* @param bool $reset
* An option to refresh all caches.
*
* @return WorkflowState[]
* An array of cached states, keyed by state_id.
*/
public static function loadMultiple(array $ids = NULL, $wid = '', $reset = FALSE) {
$states = parent::loadMultiple();
usort($states, ['Drupal\workflow\Entity\WorkflowState', 'sort'] );
// Filter onl Wid, if requested, E.g., by Workflow->getStates().
// Set the ID as array key.
$result = [];
foreach ($states as $state) {
/** @var WorkflowState $state */
if ((!$wid) || ($wid == $state->wid)) {
$result[$state->id()] = $state;
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) {
/** @var WorkflowState $a */
/** @var WorkflowState $b */
$a_wid = $a->wid;
$b_wid = $b->wid;
if ($a_wid == $b_wid) {
$a_weight = $a->getWeight();
$b_weight = $b->getWeight();
return ($a_weight < $b_weight) ? -1 : 1;
}
return ($a_wid < $b_wid) ? -1 : 1;
}
/**
* Deactivate a Workflow State, moving existing content to a given State.
*
* @param int $new_sid
* The state ID, to which all affected entities must be moved.
*/
public function deactivate($new_sid) {
$current_sid = $this->id();
$force = TRUE;
// Notify interested modules. We notify first to allow access to data before we zap it.
// - re-parents any entity that we don't want to orphan, whilst deactivating a State.
// - delete any lingering entity to state values.
// \Drupal::moduleHandler()->invokeAll('workflow', ['state delete', $current_sid, $new_sid, NULL, $force]);
// Invoke the hook.
\Drupal::moduleHandler()->invokeAll('entity_' . $this->getEntityTypeId() . '_predelete', [$this, $current_sid, $new_sid]);
// Re-parent any entity that we don't want to orphan, whilst deactivating a State.
// TODO D8-port: State should not know about Transition: move this to Workflow->DeactivateState.
if ($new_sid) {
// A candidate for the batch API.
// @TODO: Future updates should seriously consider setting this with batch.
$user = \Drupal::currentUser(); // We can use global, since deactivate() is a UI-only function.
$comment = t('Previous state deleted');
foreach (_workflow_info_fields() as $field_info) {
$entity_type_id = $field_info->getTargetEntityTypeId();
$field_name = $field_info->getName(); // @todo: overwrites key?
$result = [];
// CommentForm's are not re-parented upon Deactivate WorkflowState.
if ($entity_type_id != 'comment') {
$query = \Drupal::entityQuery($entity_type_id);
$query->condition($field_name, $current_sid, '=');
$result = $query->execute();
}
foreach ($result as $entity_id) {
$entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
$transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]);
$transition->setTargetEntity($entity);
$transition->setValues($new_sid, $user->id(), \Drupal::time()->getRequestTime(), $comment, TRUE);
$transition->force($force);
// Execute Transition, invoke 'pre' and 'post' events, save new state in Field-table, save also in workflow_transition_history.
// For Workflow Node, only {workflow_node} and {workflow_transition_history} are updated. For Field, also the Entity itself.
// Execute transition and update the attached entity.
$new_sid = $transition->executeAndUpdateEntity($force);
}
}
}
// Delete the transitions this state is involved in.
$workflow = Workflow::load($this->wid);
/** @var WorkflowInterface $workflow */
/** @var WorkflowTransitionInterface $transition */
foreach ($workflow->getTransitionsByStateId($current_sid, '') as $transition) {
$transition->delete();
}
foreach ($workflow->getTransitionsByStateId('', $current_sid) as $transition) {
$transition->delete();
}
// Delete the state. -- We don't actually delete, just deactivate.
// This is a matter up for some debate, to delete or not to delete, since this
// causes name conflicts for states. In the meantime, we just stick with what we know.
// If you really want to delete the states, use workflow_cleanup module, or delete().
$this->status = FALSE;
$this->save();
// Clear the cache.
self::loadMultiple([], 0, TRUE);
}
/**
* Property functions.
*/
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->weight;
}
/**
* Returns the Workflow ID of this State.
*
* @return string
* Workflow Id.
*/
public function getWorkflowId() {
return $this->wid;
}
/**
* Returns the Workflow object of this State.
*
* @return Workflow
* Workflow object.
*/
public function getWorkflow() {
if (!isset($this->workflow)) {
$this->workflow = Workflow::load($this->wid);
}
return $this->workflow;
}
/**
* @param Workflow $workflow
*/
public function setWorkflow(Workflow $workflow) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
$this->wid = $workflow->id();
$this->workflow = $workflow;
}
/**
* Returns the Workflow object of this State.
*
* @return bool
* TRUE if state is active, else FALSE.
*/
public function isActive() {
return (bool) $this->status;
}
/**
* Checks if the given state is the 'Create' state.
*
* @return bool
*/
public function isCreationState() {
return $this->sysid == WORKFLOW_CREATION_STATE;
}
/**
* Determines if the Workflow Form must be shown.
*
* If not, a formatter must be shown, since there are no valid options.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* @param string $field_name
* @param \Drupal\Core\Session\AccountInterface $account
* @param bool $force
*
* @return bool
* TRUE = a form (a.k.a. widget) must be shown; FALSE = no form, a formatter must be shown instead.
*/
public function showWidget(EntityInterface $entity, $field_name, AccountInterface $account, $force) {
$options = $this->getOptions($entity, $field_name, $account, $force);
$count = count($options);
// The easiest case first: more then one option: always show form.
if ($count > 1) {
return TRUE;
}
// #2226451: Even in Creation state, we must have 2 visible states to show the widget.
// // Only when in creation phase, one option is sufficient,
// // since the '(creation)' option is not included in $options.
// // When in creation state,
// if ($this->isCreationState()) {
// return TRUE;
// }
return FALSE;
}
/**
* Returns the allowed transitions for the current state.
*
* @param \Drupal\Core\Entity\EntityInterface|null $entity
* The entity at hand. May be NULL (E.g., on a Field settings page).
* @param string $field_name
* @param \Drupal\Core\Session\AccountInterface|null $account
* @param bool|false $force
*
* @return \Drupal\workflow\Entity\WorkflowConfigTransition[]
* An array of id=>transition pairs with allowed transitions for State.
*/
public function getTransitions(EntityInterface $entity = NULL, $field_name = '', AccountInterface $account = NULL, $force = FALSE) {
$transitions = [];
if (!$workflow = $this->getWorkflow()) {
// No workflow, no options ;-)
return $transitions;
}
// @todo: Keep below code aligned between WorkflowState, ~Transition, ~TransitionListController
/**
* Get permissions of user, adding a Role to user, depending on situation.
*/
// Load a User object, since we cannot add Roles to AccountInterface.
/** @var \Drupal\user\UserInterface $user */
$user = workflow_current_user($account);
// Determine if user is owner of the entity.
$is_owner = WorkflowManager::isOwner($user, $entity);
// Check allow-ability of state change if user is not superuser (might be cron)
$type_id = $this->getWorkflowId();
if ($user->hasPermission("bypass $type_id workflow_transition access")) {
// Superuser is special. And $force allows Rules to cause transition.
$force = TRUE;
}
if ($is_owner) {
$user->addRole(WORKFLOW_ROLE_AUTHOR_RID);
}
/**
* Get the object and its permissions.
*/
/** @var WorkflowConfigTransition[] $transitions */
$transitions = $workflow->getTransitionsByStateId($this->id(), '');
/**
* Determine if user has Access.
*/
// Use default module permissions.
foreach ($transitions as $key => $transition) {
if (!$transition->isAllowed($user, $force)) {
unset($transitions[$key]);
}
}
// Let custom code add/remove/alter the available transitions,
// using the new drupal_alter.
// Modules may veto a choice by removing a transition from the list.
// Lots of data can be fetched via the $transition object.
$context = [
'entity' => $entity, // ConfigEntities do not have entity attached
'field_name' => $field_name, // or field.
'user' => $user, // user may have the custom role AUTHOR.
'workflow' => $workflow,
'state' => $this,
'force' => $force,
];
\Drupal::moduleHandler()->alter('workflow_permitted_state_transitions', $transitions, $context);
/**
* Determine if user has Access.
*/
// As of 8.x-1.x, below hook() is removed, in favour of above alter().
// Let custom code change the options, using old_style hook.
// Above drupal_alter() calls hook_workflow_permitted_state_transitions_alter() only once.
// foreach ($transitions as $transition) {
// $to_sid = $transition->to_sid;
// $permitted = [];
//
// // We now have a list of config_transitions. Check each against the Entity.
// // Invoke a callback indicating that we are collecting state choices.
// // Modules may veto a choice by returning FALSE.
// // In this case, the choice is never presented to the user.
// if (!$force) {
// // @todo: D8-port: simplify interface for workflow_hook. Remove redundant context.
// $permitted = \Drupal::moduleHandler()->invokeAll('workflow', ['transition permitted', $transition, $user]);
// }
//
// // If vetoed by a module, remove from list.
// if (in_array(FALSE, $permitted, TRUE)) {
// unset($transitions[$transition->id()]);
// }
// }
return $transitions;
}
/**
* Returns the allowed values for the current state.
*
* @param object $entity
* The entity at hand. May be NULL (E.g., on a Field settings page).
* @param string $field_name
* @param \Drupal\Core\Session\AccountInterface|null $account
* @param bool $force
*
* @return array
* An array of sid=>label pairs.
* If $this->id() is set, returns the allowed transitions from this state.
* If $this->id() is 0 or FALSE, then labels of ALL states of the State's
* Workflow are returned.
*/
public function getOptions($entity, $field_name, AccountInterface $account = NULL, $force = FALSE) {
$options = [];
// Define an Entity-specific cache per page load.
static $cache = [];
$entity_id = ($entity) ? $entity->id() : '';
$entity_type_id = ($entity) ? $entity->getEntityTypeId() : '';
$current_sid = $this->id();
// Get options from page cache, using a non-empty index (just to be sure).
$entity_index = (!$entity) ? 'x' : $entity_id;
if (isset($cache[$entity_type_id][$entity_index][$force][$current_sid])) {
$options = $cache[$entity_type_id][$entity_index][$force][$current_sid];
return $options;
}
$workflow = $this->getWorkflow();
if (!$workflow) {
// No workflow, no options ;-)
$options = [];
}
elseif (!$current_sid) {
// If no State ID is given, we return all states.
// We cannot use getTransitions, since there are no ConfigTransitions
// from State with ID 0, and we do not want to repeat States.
/** @var WorkflowState $state */
foreach ($workflow->getStates() as $state) {
$options[$state->id()] = html_entity_decode(t('@label', ['@label' => $state->label()]));
}
}
else {
$transitions = $this->getTransitions($entity, $field_name, $account, $force);
foreach ($transitions as $transition) {
// Get the label of the transition, and if empty of the target state.
// Beware: the target state may not exist, since it can be invented
// by custom code in the above drupal_alter() hook.
if (!$label = $transition->label()) {
$to_state = $transition->getToState();
$label = $to_state ? $to_state->label() : '';
}
$to_sid = $transition->to_sid;
$options[$to_sid] = html_entity_decode(t('@label', ['@label' => $label]));
}
// Save to entity-specific cache.
$cache[$entity_type_id][$entity_index][$force][$current_sid] = $options;
}
return $options;
}
/**
* Returns the number of entities with this state.
*
* @return int
* Counted number.
*
* @todo: add $options to select on entity type, etc.
*/
public function count() {
$count = 0;
$sid = $this->id();
foreach ($fields = _workflow_info_fields() as $field_info) {
$field_name = $field_info->getName();
$query = \Drupal::entityQuery($field_info->getTargetEntityTypeId());
// @see #2285983 for using SQLite on D7.
$count += $query
->condition($field_name, $sid, '=')
->count() // We only need the count.
->execute();
}
return $count;
}
}
@@ -0,0 +1,107 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Storage handler for workflow config entities.
*/
class WorkflowStorage extends ConfigEntityStorage {
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $stateService;
/**
* Constructs a WorkflowStorage object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Component\Uuid\UuidInterface $uuid_service
* The UUID service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\State\StateInterface $state_service
* The state service.
*/
public function __construct(EntityTypeInterface $entity_type, ConfigFactoryInterface $config_factory, UuidInterface $uuid_service, LanguageManagerInterface $language_manager, StateInterface $state_service) {
parent::__construct($entity_type, $config_factory, $uuid_service, $language_manager);
$this->stateService = $state_service;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('config.factory'),
$container->get('uuid'),
$container->get('language_manager'),
$container->get('state')
);
}
/**
* Returns a list of event names that are used by active workflows.
*
* @return string[]
* The list of event names keyed by event name.
*/
protected function getRegisteredEvents() {
// $events = [];
// foreach ($this->loadMultiple() as $rules_config) {
// $event = $rules_config->getEvent();
// if ($event && !isset($events[$event])) {
// $events[$event] = $event;
// }
// }
// return $events;
}
/**
* {@inheritdoc}
*/
public function doLoadMultiple(array $ids = NULL) {
$return = parent::doLoadMultiple($ids);
return $return;
}
/**
* {@inheritdoc}
*/
public function save(EntityInterface $entity) {
$return = parent::save($entity);
// // Update the state of registered events.
// // @todo Should we trigger a container rebuild here as well? Might be a bit
// // expensive on every save?
// $this->stateService->set('rules.registered_events', $this->getRegisteredEvents());
return $return;
}
/**
* {@inheritdoc}
*/
public function delete(array $entities) {
$return = parent::delete($entities);
return $return;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,277 @@
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Entity\EntityInterface;
use Drupal\user\EntityOwnerInterface;
/**
* Defines a common interface for Workflow*Transition* objects.
*
* @see \Drupal\workflow\Entity\WorkflowConfigTransition
* @see \Drupal\workflow\Entity\WorkflowTransition
* @see \Drupal\workflow\Entity\WorkflowScheduledTransition
*/
interface WorkflowTransitionInterface extends WorkflowConfigTransitionInterface, EntityInterface, EntityOwnerInterface {
/**
* Helper function for __construct. Used for all children of WorkflowTransition (aka WorkflowScheduledTransition)
*
* Usage:
* $transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]);
* $transition->setTargetEntity($entity);
* $transition->setValues($new_sid, $user->id(), REQUEST_TIME, $comment);
*
* @param string $to_sid
* @param int $uid
* @param int $timestamp
* @param string $comment
* @param bool $force_create
*/
public function setValues($to_sid, $uid = NULL, $timestamp = NULL, $comment = '', $force_create = FALSE);
/**
* Load (Scheduled) WorkflowTransitions, most recent first.
*
* @param string $entity_type
* @param int $entity_id
* @param array $revision_ids
* @param string $field_name
* @param string $langcode
* @param string $sort
* @param string $transition_type
*
* @return \Drupal\workflow\Entity\WorkflowTransitionInterface
* Object representing one row from the {workflow_transition_history} table.
*/
public static function loadByProperties($entity_type, $entity_id, array $revision_ids = [], $field_name = '', $langcode = '', $sort = 'ASC', $transition_type = '');
/**
* Given an entity, get all transitions for it.
*
* Since this may return a lot of data, a limit is included to allow for only one result.
*
* @param string $entity_type
* @param int[] $entity_ids
* @param int[] $revision_ids
* @param string $field_name
* Optional. Can be NULL, if you want to load any field.
* @param string $langcode
* Optional. Can be empty, if you want to load any language.
* @param int $limit
* Optional. Can be NULL, if you want to load all transitions.
* @param string $sort
* Optional sort order. {'ASC'|'DESC'}
* @param string $transition_type
* The type of the transition to be fetched.
*
* @return WorkflowTransitionInterface[]
* An array of transitions.
*/
public static function loadMultipleByProperties($entity_type, array $entity_ids, array $revision_ids = [], $field_name = '', $langcode = '', $limit = NULL, $sort = 'ASC', $transition_type = '');
/**
* Execute a transition (change state of an entity).
*
* A Scheduled Transition shall only be saved, unless the
* 'schedule' property is set.
* @usage
* $transition->schedule(FALSE);
* $to_sid = $transition->execute(TRUE);
*
* @param bool $force
* If set to TRUE, workflow permissions will be ignored.
*
* @return
* New state ID. If execution failed, old state ID is returned,
*/
public function execute($force = FALSE);
/**
* Executes a transition (change state of an entity), from OUTSIDE the entity.
*
* Use $transition->executeAndUpdateEntity() to start a State Change from
* outside an entity, e.g., workflow_cron().
* Use $transition->execute() to start a State Change from within an entity.
*
* A Scheduled Transition ($transition->isScheduled() == TRUE) will be
* un-scheduled and saved in the history table.
* The entity will not be updated.
* If $transition->isScheduled() == FALSE, the Transition will be
* removed from the {workflow_transition_scheduled} table (if necessary),
* and added to {workflow_transition_history} table.
* Then the entity wil be updated to reflect the new status.
*
* @usage
* $to_sid = $transition->->executeAndUpdateEntity($force);
*
* @see workflow_execute_transition()
*
* @param bool $force
* If set to TRUE, workflow permissions will be ignored.
*
* @return string
* The resulting WorkflowState id.
*/
public function executeAndUpdateEntity($force = FALSE);
/**
* Invokes 'transition post'.
* Adds the possibility to invoke the hook from elsewhere.
*
* @param bool $force
*/
public function post_execute($force = FALSE);
/**
* Set the Entity, that is added to the Transition.
* Also set all dependent fields, that will be saved in tables {workflow_transition_*}
*
* @param EntityInterface $entity
* The Entity ID or the Entity object, to add to the Transition.
*
* @return object
* The Entity, that is added to the Transition.
*/
public function setTargetEntity($entity);
/**
* Returns the entity to which the workflow is attached.
*
* @return EntityInterface
* The entity to which the workflow is attached.
*/
public function getTargetEntity();
/**
* Returns the ID of the entity to which the workflow is attached.
*
* @return int
* The ID of the entity to which the workflow is attached.
*/
public function getTargetEntityId();
/**
* Returns the type of the entity to which the workflow is attached.
*
* @return string
* An entity type.
*/
public function getTargetEntityTypeId();
/**
* Get the field_name for which the Transition is valid.
*
* @return string
* The field_name, that is added to the Transition.
*/
public function getFieldName();
/**
* Get the language code for which the Transition is valid.
*
* @todo: OK?? Shouldn't we use entity's language() method for langcode?
*
* @return string $langcode
*/
public function getLangcode();
/**
* Get the comment of the Transition.
*
* @return
* The comment
*/
public function getComment();
/**
* Get the comment of the Transition.
*
* @param $value
* The new comment.
*
* @return WorkflowTransitionInterface
*/
public function setComment($value);
/**
* Returns the time on which the transitions was or will be executed.
*
* @return
*/
public function getTimestamp();
/**
* Returns the human-readable time.
*
* @return string
*/
public function getTimestampFormatted();
/**
* Returns the time on which the transitions was or will be executed.
*
* @param $value
* The new timestamp.
* @return WorkflowTransitionInterface
*/
public function setTimestamp($value);
/**
* Returns if this is a Scheduled Transition.
*
* @return bool
*/
public function isScheduled();
/**
* Sets the Transition to be scheduled or not.
*
* @param bool $schedule
* @return WorkflowTransitionInterface
*/
public function schedule($schedule = TRUE);
/**
* Set the 'is_executed' property.
*
* @param bool $is_executed
*
* @return WorkflowTransitionInterface
*/
public function setExecuted($is_executed = TRUE);
/**
* Returns if this is an Executed Transition.
*
* @return bool
*/
public function isExecuted();
/**
* A transition may be forced skipping checks.
*
* @return bool
* If the transition is forced. (Allow not-configured transitions).
*/
public function isForced();
/**
* Set if a transition must be executed, even if transition is invalid
* or user not authorized.
*
* @param bool $force
*
* @return object
* The transition itself
*/
public function force($force = TRUE);
/**
* Helper/debugging function. Shows simple contents of Transition.
*
* @param string $function
*/
public function dpm($function = '');
}
@@ -0,0 +1,211 @@
<?php
namespace Drupal\workflow\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflow\Element\WorkflowTransitionElement;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
/**
* Provides a Transition Form to be used in the Workflow Widget.
*/
class WorkflowTransitionForm extends ContentEntityForm {
/*************************************************************************
*
* Implementation of interface FormInterface.
*
*/
/**
* {@inheritdoc}
*/
public function getFormId() {
// We need a proprietary Form ID, to identify the unique forms
// when multiple fields or entities are shown on 1 page.
// Test this f.i. by checking the'scheduled' box. It wil
// not unfold.
$form_id = parent::getFormId();
/** @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */
$transition = $this->entity;
$field_name = $transition->getFieldName();
/** @var $entity \Drupal\Core\Entity\EntityInterface */
// Entity may be empty on VBO bulk form.
// $entity = $transition->getTargetEntity();
// Compose Form Id from string + Entity Id + Field name.
// Field ID contains entity_type, bundle, field_name.
// The Form Id is unique, to allow for multiple forms per page.
// $workflow_type_id = $transition->getWorkflowId();
// Field name contains implicit entity_type & bundle (since 1 field per entity)
// $entity_type = $transition->getTargetEntityTypeId();
// $entity_id = $transition->getTargetEntityId();;
// Emulate nodeForm convention.
if ($transition->id()) {
$suffix = 'edit_form';
}
else {
$suffix = 'form';
}
$form_id = implode('_', ['workflow_transition', $field_name, $suffix]);
$form_id = Html::getUniqueId($form_id);
return $form_id;
}
/**
* {@inheritdoc}
*
* N.B. The D8-version of this form is stripped. If any use case is missing:
* - compare with the D7-version of WorkflowTransitionForm::submitForm()
* - compare with the D8-version of WorkflowTransitionElement::copyFormValuesToEntity()
*/
/*
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
}
*/
/* *************************************************************************
*
* Implementation of interface EntityFormInterface (extends FormInterface).
*
*/
/**
* This function is called by buildForm().
* Caveat: !! It is not declared in the EntityFormInterface !!
*
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
// Call parent to get (extra) fields.
// This might cause baseFieldDefinitions to appear twice.
$form = parent::form($form, $form_state);
/** @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */
$transition = $this->entity;
// Do not pass the element, but the form.
// $element['#default_value'] = $transition;
// $form += WorkflowTransitionElement::transitionElement($element, $form_state, $form);
//
// Pass the form via parameter
$form['#default_value'] = $transition;
$form = WorkflowTransitionElement::transitionElement($form, $form_state, $form);
return $form;
}
/*
* Returns the action form element for the current entity form.
* Caveat: !! It is not declared in the EntityFormInterface !!
*
* {@inheritdoc}
protected function actionsElement(array $form, FormStateInterface $form_state) {
$element = parent::actionsElement($form, $form_state);
return $element;
}
*/
/**
* Returns an array of supported actions for the current entity form.
* Caveat: !! It is not declared in the EntityFormInterface !!
* @param array $form
* @param \Drupal\Core\Form\FormStateInterface $form_state
* @return array
*/
protected function actions(array $form, FormStateInterface $form_state) {
// N.B. Keep code aligned: workflow_form_alter(), WorkflowTransitionForm::actions().
$actions = parent::actions($form, $form_state);
// A default button is provided by core. Override it.
$actions['submit']['#value'] = t('Update workflow');
$actions['submit']['#attributes'] = ['class' => ['form-save-default-button']];
if (!_workflow_use_action_buttons()) {
// Change the default submit button on the Workflow History tab.
return $actions;
}
// Find the first workflow.
// (So this won't work with multiple workflows per entity.)
// Quit if there is no Workflow on this page.
if (!$workflow_form = &$form) {
return $actions;
}
// Quit if there are no Workflow Action buttons.
// (If user has only 1 workflow option, there are no Action buttons.)
if (count($workflow_form['to_sid']['#options']) <= 1) {
return $actions;
}
// Place the buttons. Remove the default 'Save' button.
// $actions += _workflow_transition_form_get_action_buttons($form, $workflow_form);
// Remove the default submit button from the form.
// unset($actions['submit']);
$default_submit_action = $actions['submit'];
$actions = _workflow_transition_form_get_action_buttons($form, $workflow_form, $default_submit_action);
foreach ($actions as $key => &$action) {
$action['#submit'] = $default_submit_action['#submit'];
}
return $actions;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
$entity = clone $this->entity;
// Update the entity.
$entity = $this->copyFormValuesToEntity($entity, $form, $form_state);
// Mark the entity as NOT requiring validation. (Used in validateForm().)
$entity->setValidationRequired(FALSE);
return $entity;
}
/**
* {@inheritdoc}
*/
public function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
parent::copyFormValuesToEntity($entity, $form, $form_state);
// Use a proprietary version of copyFormValuesToEntity(), passing $entity by reference...
$values = $form_state->getValues();
// ... but only the returning object is OK (!).
return WorkflowTransitionElement::copyFormValuesToTransition($entity, $form, $form_state, $values);
}
/**
* {@inheritdoc}
*
* This is called from submitForm().
*/
public function save(array $form, FormStateInterface $form_state) {
// Execute transition and update the attached entity.
/** @var WorkflowTransitionInterface $transition */
$transition = $this->getEntity();
return $transition->executeAndUpdateEntity();
}
/*************************************************************************
*
* Implementation of interface ContentEntityFormInterface (extends EntityFormInterface).
*
*/
/**
* {@inheritdoc}
*/
//public function validateForm(array &$form, FormStateInterface $form_state) {
// return parent::validateForm($form, $form_state);
//}
}
@@ -0,0 +1,247 @@
<?php
namespace Drupal\workflow\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflow\Entity\Workflow;
/**
* Provides the base form for workflow add and edit forms.
*/
class WorkflowTypeForm extends EntityForm {
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$noyes = [0 => t('No'), 1 => t('Yes')];
$fieldset_options = [0 => t('No fieldset'), 1 => t('Collapsible fieldset'), 2 => t('Collapsed fieldset')];
/* @var $workflow Workflow */
$workflow = $this->entity;
$form['label'] = [
'#type' => 'textfield',
'#description' => t('The human-readable label of the workflow. This will be used as a label when
the workflow status is shown during editing of content.'),
'#title' => $this->t('Label'),
'#default_value' => $this->entity->label(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'),
'#disabled' => !$this->entity->isNew(),
'#default_value' => $this->entity->id(),
'#machine_name' => [
'exists' => [$this, 'exists'],
'replace_pattern' => '([^a-z0-9_]+)|(^custom$)',
'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".'),
],
];
$form['permissions'] = [
'#type' => 'details',
'#title' => t('Workflow permissions'),
'#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE.
'#description' => t("To enable further Workflow functionality, go to the
/admin/people/permissions page and select any roles that should have
access to below and other functionalities."),
];
$form['permissions']['transition_execute'] = [
'#type' => 'item',
'#title' => t('Participate in workflow (create, execute transitions)'),
'#markup' => t("You can determine which roles are enabled on the
'Workflow Transitions & roles' configuration page. Use this to enable
only the relevant roles. Some sites have too many roles to show on
the configuration page."),
];
$form['permissions']['transition_schedule'] = [
'#type' => 'item',
'#title' => t('Schedule state transition'),
'#markup' => t("Workflow transitions may be scheduled to a moment in the
future. Soon after the desired moment, the transition is executed by
Cron. This may be hidden by settings in widgets, formatters or permissions."
),
];
$form['permissions']['history_tab'] = [
'#type' => 'item',
'#title' => t('Access Workflow history tab'),
'#markup' => t("You can determine if a tab 'Workflow history' is
shown on the entity view page, which gives access to the History of
the workflow.
If you have multiple workflows per bundle, better disable this feature,
and use, clone & adapt the Views display 'Workflow history per Entity'."),
];
$form['basic'] = [
'#type' => 'details',
'#title' => t('Workflow form settings'),
// '#description' => t('Lorem ipsum.'),
'#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE.
];
$form['basic']['fieldset'] = [
'#type' => 'select',
'#options' => $fieldset_options,
'#title' => t('Show the form in a fieldset?'),
'#default_value' => isset($workflow->options['fieldset']) ? $workflow->options['fieldset'] : 0,
];
$form['basic']['options'] = [
'#type' => 'select',
'#title' => t('How to show the available states'),
'#required' => FALSE,
'#default_value' => isset($workflow->options['options']) ? $workflow->options['options'] : 'radios',
// '#multiple' => TRUE / FALSE,
'#options' => [
// These options are taken from options.module
'select' => 'Select list',
'radios' => 'Radio buttons',
'buttons' => 'Action buttons',
'dropbutton' => 'Drop button',
],
'#description' => t("The Widget shows all available states. Decide which
is the best way to show them."
),
];
$form['basic']['name_as_title'] = [
'#type' => 'checkbox',
'#attributes' => ['class' => ['container-inline']],
'#title' => t('Use the workflow name as the title of the workflow form'),
'#default_value' => isset($workflow->options['name_as_title']) ? $workflow->options['name_as_title'] : 0,
'#description' => t(
'The workflow section of the editing form is in its own fieldset.
Checking the box will add the workflow name as the title of workflow
section of the editing form.'
),
];
$form['basic']['schedule_timezone'] = [
'#type' => 'checkbox',
'#title' => t('Show a timezone when scheduling a transition.'),
'#required' => FALSE,
'#default_value' => isset($workflow->options['schedule_timezone']) ? $workflow->options['schedule_timezone'] : 1,
];
$form['basic']['comment_log_node'] = [
'#type' => 'select',
'#required' => FALSE,
'#options' => [
// Use 0/1/2 to stay compatible with previous checkbox.
0 => t('hidden'),
1 => t('optional'),
2 => t('required'),
],
'#attributes' => ['class' => ['container-inline']],
'#title' => t('How to show the Comment subfield'),
'#default_value' => isset($workflow->options['comment_log_node']) ? $workflow->options['comment_log_node'] : 1,
'#description' => t(
'A Comment area can be shown on the Workflow Transition form so that the person
making a state change can record reasons for doing so. The comment is
then included in the content\'s workflow history.'
),
];
$form['watchdog'] = [
'#type' => 'details',
'#title' => t('Watchdog'),
'#description' => t('Informational watchdog messages can be logged when a transition is executed (state of a node is changed).'),
'#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE.
];
$form['watchdog']['watchdog_log'] = [
'#type' => 'checkbox',
'#title' => t('Log watchdog messages upon state change'),
'#default_value' => isset($workflow->options['watchdog_log']) ? $workflow->options['watchdog_log'] : 0,
'#description' => '',
];
return parent::form($form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
// $actions['submit']['#value'] = $this->t('Save');
return $actions;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
/* @var $entity \Drupal\workflow\Entity\Workflow */
$entity = $this->entity;
// Prevent leading and trailing spaces.
$entity->set('label', trim($entity->label()));
$entity->set('options', [
'name_as_title' => $form_state->getValue('name_as_title'),
'fieldset' => $form_state->getValue('fieldset'),
'options' => $form_state->getValue('options'),
'schedule_timezone' => $form_state->getValue('schedule_timezone'),
'comment_log_node' => $form_state->getValue('comment_log_node'),
'watchdog_log' => $form_state->getValue('watchdog_log'),
]
);
$status = parent::save($form, $form_state);
$action = $status == SAVED_UPDATED ? 'updated' : 'added';
// Tell the user we've updated the data.
$args = [
'%label' => $entity->label(),
'%action' => $action,
'link' => $entity->toLink(t('Edit'))->toString(),
];
drupal_set_message($this->t('Workflow %label has been %action. Please maintain the permissions, states and transitions.', $args));
$this->logger('workflow')->notice('Workflow %label has been %action.', $args);
if ($status == SAVED_NEW) {
$form_state->setRedirect('entity.workflow_type.edit_form', ['workflow_type' => $entity->id()]);
}
}
/**
* Form validation handler.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$workflow = $this->entity;
$name = $workflow->id();
// Make sure workflow name is not numeric.
// TODO: this was a prerequisite in D7. Remove in D8?
if (ctype_digit($name)) {
$form_state->setErrorByName('id', t('Please choose a non-numeric name for your workflow.'));
}
parent::validateForm($form, $form_state);
}
/**
* Machine name exists callback.
*
* @param string $id
* The machine name ID.
*
* @return bool
* TRUE if an entity with the same name already exists, FALSE otherwise.
*/
public function exists($id) {
$type = $this->entity->getEntityTypeId();
return (bool) $this->entityTypeManager->getStorage($type)->load($id);
}
}
@@ -0,0 +1,79 @@
<?php
namespace Drupal\workflow\Plugin\Action;
use Drupal\Core\Form\FormStateInterface;
/**
* Sets an entity to a new, given state.
*
* The only change is the 'type' in tha Annotation, so it works on Nodes,
* and can be seen on admin/content page.
*
* @Action(
* id = "workflow_node_given_state_action",
* label = @Translation("Change a node to new Workflow state"),
* type = "node"
* )
*/
class WorkflowNodeGivenStateAction extends WorkflowStateActionBase {
/**
* {@inheritdoc}
*/
public function calculateDependencies(){
return [
'module' => ['workflow', 'node'],
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
/*
* D7: As advanced action with Trigger 'node':
* - $entity is empty;
* - $context['group'] = 'node'
* - $context['hook'] = 'node_insert / _update / _delete'
* - $context['node'] = (Object) stdClass
* - $context['entity_type'] = NULL
*
* D7: As advanced action with Trigger 'taxonomy':
* - $entity is (Object) stdClass;
* - $context['type'] = 'entity'
* - $context['group'] = 'taxonomy'
* - $context['hook'] = 'taxonomy_term_insert / _update / _delete'
* - $context['node'] = (Object) stdClass
* - $context['entity_type'] = NULL
*
* D7: As advanced action with Trigger 'workflow API':
* ...
*
* D7: As VBO action:
* - $entity is (Object) stdClass;
* - $context['type'] = NULL
* - $context['group'] = NULL
* - $context['hook'] = NULL
* - $context['node'] = (Object) stdClass
* - $context['entity_type'] = 'node'
*/
if (!$transition = $this->getTransitionForExecution($object)) {
drupal_set_message('The entity is not valid for this action.');
return;
}
// Fire the transition.
workflow_execute_transition($transition, $force);
}
}
@@ -0,0 +1,71 @@
<?php
namespace Drupal\workflow\Plugin\Action;
use Drupal\Core\Form\FormStateInterface;
/**
* Sets an entity to the next state.
*
* The only change is the 'type' in tha Annotation, so it works on Nodes,
* and can be seen on admin/content page.
*
* @Action(
* id = "workflow_node_next_state_action",
* label = @Translation("Change a node to next Workflow state"),
* type = "node"
* )
*/
class WorkflowNodeNextStateAction extends WorkflowStateActionBase {
/**
* {@inheritdoc}
*/
public function calculateDependencies(){
return [
'module' => ['workflow', 'node'],
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// Remove to_sid. User can't set it, since we want a dynamic 'next' state.
unset($form['to_sid']);
return $form;
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
if (!$transition = $this->getTransitionForExecution($object)) {
drupal_set_message('The object is not valid for this action.', 'warning');
return;
}
/*
* Set the new next state.
*/
$entity = $transition->getTargetEntity();
$field_name = $transition->getFieldName();
$user = $transition->getOwner();
$force = $this->configuration['force'];
// $comment = $transition->getComment();
// Get the node's new State Id (which is the next available state).
$to_sid = $transition->getWorkflow()->getNextSid($entity, $field_name, $user, $force);
// Add actual data.
$transition->to_sid = $to_sid;
// Fire the transition.
workflow_execute_transition($transition, $force);
}
}
@@ -0,0 +1,232 @@
<?php
namespace Drupal\workflow\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\workflow\Element\WorkflowTransitionElement;
use Drupal\workflow\Entity\Workflow;
use Drupal\workflow\Entity\WorkflowTransition;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Sets an entity to a new, given state.
*
* Example Annotation @ Action(
* id = "workflow_given_state_action",
* label = @Translation("Change a node to new Workflow state"),
* type = "workflow"
* )
*/
abstract class WorkflowStateActionBase extends ConfigurableActionBase implements ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return parent::calculateDependencies() + [
'module' => ['workflow',],
];
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$configuration = parent::defaultConfiguration();
$configuration += $this->configuration;
$configuration += [
'field_name' => '',
'to_sid' => '',
'comment' => "New state is set by a triggered Action.",
'force' => 0,
];
return $configuration;
}
/**
* @param EntityInterface $entity
* @return \Drupal\workflow\Entity\WorkflowTransitionInterface
*/
protected function getTransitionForExecution(EntityInterface $entity) {
$user = workflow_current_user();
if (!$entity) {
\Drupal::logger('workflow_action')->notice('Unable to get current entity - entity is not defined.', []);
return NULL;
}
// Get the entity type and numeric ID.
$entity_id = $entity->id();
if (!$entity_id) {
\Drupal::logger('workflow_action')->notice('Unable to get current entity ID - entity is not yet saved.', []);
return NULL;
}
// In 'after saving new content', the node is already saved. Avoid second insert.
// @todo: clone?
$entity->enforceIsNew(FALSE);
$config = $this->configuration;
$field_name = workflow_get_field_name($entity, $config['field_name']);
$current_sid = workflow_node_current_state($entity, $field_name);
if (!$current_sid) {
\Drupal::logger('workflow_action')->notice('Unable to get current workflow state of entity %id.', ['%id' => $entity_id]);
return NULL;
}
$to_sid = isset($config['to_sid']) ? $config['to_sid'] : '';
// Get the Comment. Parse the $comment variables.
$comment_string = $this->configuration['comment'];
$comment = t($comment_string, [
'%title' => $entity->label(),
// "@" and "%" will automatically run check_plain().
'%state' => workflow_get_sid_name($to_sid),
'%user' => $user->getDisplayName(),
]);
$force = $this->configuration['force'];
$transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]);
$transition->setTargetEntity($entity);
$transition->setValues($to_sid, $user->id(), \Drupal::time()->getRequestTime(), $comment);
$transition->force($force);
return $transition;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = [];
// If we are on admin/config/system/actions and use CREATE AN ADVANCED ACTION
// Then $context only contains:
// - $context['actions_label'] = "Change workflow state of post to new state"
// - $context['actions_type'] = "entity"
//
// If we are on a VBO action form, then $context only contains:
// - $context['entity_type'] = "node"
// - $context['view'] = "(Object) view"
// - $context['settings'] = "[]"
$config = $this->configuration;
$field_name = $config['field_name'];
$wids = workflow_get_workflow_names();
if (empty($field_name) && count($wids) > 1) {
drupal_set_message('You have more then one workflow in the system. Please first select the field name
and save the form. Then, revisit the form to set the correct state value.', 'warning');
}
if (empty($field_name)) {
$wid = count($wids) ? array_keys($wids)[0] : '';
}
else {
$fields = _workflow_info_fields($entity = NULL, $entity_type = '', $entity_bundle = '', $field_name);
$wid = count($fields) ? reset($fields)->getSetting('workflow_type') : '';
}
// Get the common Workflow, or create a dummy Workflow.
$workflow = $wid ? Workflow::load($wid) : Workflow::create(['id' => 'dummy_action', 'label' => 'dummy_action']);
$current_state = $workflow->getCreationState();
/*
// @todo D8-port for VBO
// Show the current state and the Workflow form to allow state changing.
// N.B. This part is replicated in hook_node_view, workflow_tab_page, workflow_vbo.
if ($workflow) {
$field = _workflow_info_field($field_name, $workflow);
$field_name = $field['field_name'];
$field_id = $field['id'];
$instance = field_info_instance($entity_type, $field_name, $entity_bundle);
// Hide the submit button. VBO has its own 'next' button.
$instance['widget']['settings']['submit_function'] = '';
if (!$field_id) {
// This is a Workflow Node workflow. Set widget options as in v7.x-1.2
$field['settings']['widget']['comment'] = $workflow->options['comment_log_node']; // 'comment_log_tab' is removed;
$field['settings']['widget']['current_status'] = TRUE;
// As stated above, the options list is probably very long, so let's use select list.
$field['settings']['widget']['options'] = 'select';
// Do not show the default [Update workflow] button on the form.
$instance['widget']['settings']['submit_function'] = '';
}
}
// Add the form/widget to the formatter, and include the nid and field_id in the form id,
// to allow multiple forms per page (in listings, with hook_forms() ).
// Ultimately, this is a wrapper for WorkflowDefaultWidget.
// $form['workflow_current_state'] = workflow_state_formatter($entity_type, $entity, $field, $instance);
$form_id = implode('_', [
'workflow_transition_form',
$entity_type,
$entity_id,
$field_id
]);
*/
$user = workflow_current_user();
$transition = WorkflowTransition::create([$current_state, 'field_name' => $field_name]);
$transition->setValues(
$to_sid = $config['to_sid'],
$user->id(),
\Drupal::time()->getRequestTime(),
$comment = $config['comment'],
$force = $config['force']
);
// Add the WorkflowTransitionForm to the page. @todo
$element = []; // Just to be explicit.
$element['#default_value'] = $transition;
// Avoid Action Buttons. That removes the options box&more. No Buttons in config screens!
$original_options = $transition->getWorkflow()->options['options'];
$transition->getWorkflow()->options['options'] = 'select';
// Generate and add the Workflow form element.
$element = WorkflowTransitionElement::transitionElement($element, $form_state, $form);
// Just to be sure, reset the options box setting.
$transition->getWorkflow()->options['options'] = $original_options;
// Make adaptations for VBO-form:
$element['field_name']['#access'] = TRUE;
$element['force']['#access'] = TRUE;
$element['to_sid']['#description'] = t('Please select the state that should be assigned when this action runs.');
$element['comment']['#title'] = $this->t('Message');
$element['comment']['#description'] = $this->t('This message will be written into the workflow history log when the action
runs. You may include the following variables: %state, %title, %user.');
$form['workflow_transition_action_config'] = $element;
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$configuration = $form_state->getValue('workflow_transition_action_config');
// Remove the transition: generates an error upon saving the action definition.
unset($configuration['workflow_transition']);
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$access = AccessResult::allowed();
return $return_as_object ? $access : $access->isAllowed();
}
}
@@ -0,0 +1,85 @@
<?php
namespace Drupal\workflow\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\workflow\Entity\WorkflowManager;
/**
* Provides a 'Workflow Transition form' block.
* Credits to workflow_extensions module.
*
* @todo D8-port: add cache options in configuration.
* 'cache' => DRUPAL_NO_CACHE, // DRUPAL_CACHE_PER_ROLE will be assumed.
*
* @Block(
* id = "workflow_transition_form_block",
* admin_label = @Translation("Workflow Transition form"),
* category = @Translation("Forms")
* )
*/
class WorkflowTransitionBlock extends BlockBase {
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
/* @var $entity \Drupal\Core\Entity\EntityInterface */
if (!$entity = workflow_url_get_entity()) {
return AccessResult::forbidden();
}
// Only show block on entity view page (when default operation = '').
if ($operation = workflow_url_get_operation()) {
return AccessResult::forbidden();
}
// Only show block if entity has workflow, and user has permission.
foreach (_workflow_info_fields($entity) as $definition) {
$type_id = $definition->getSetting('workflow_type');
if ($account->hasPermission("access $type_id workflow_transition form")) {
return AccessResult::allowed();
}
}
return AccessResult::forbidden();
}
/**
* {@inheritdoc}
*/
public function build() {
$form = [];
// Get the entity for this form.
/* @var $entity \Drupal\Core\Entity\EntityInterface */
if (!$entity = workflow_url_get_entity()) {
return $form;
}
// Get the field name. Avoid error on Node Add page.
if (!$field_name = workflow_get_field_name($entity)) {
return $form;
}
/*
* Output: generate the Transition Form.
*/
// Add the WorkflowTransitionForm to the page.
$form = WorkflowManager::getWorkflowTransitionForm($entity, $field_name);
return $form;
}
/**
* Retrieves the entity form builder.
*
* @return \Drupal\Core\Entity\EntityFormBuilderInterface
* The entity form builder.
*/
protected function entityFormBuilder() {
return \Drupal::getContainer()->get('entity.form_builder');
}
}
@@ -0,0 +1,206 @@
<?php
namespace Drupal\workflow\Plugin\Field\FieldFormatter;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\workflow\Entity\WorkflowManager;
use Drupal\workflow\Entity\WorkflowState;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a default workflow formatter.
*
* @FieldFormatter(
* id = "workflow_default",
* module = "workflow",
* label = @Translation("Workflow form"),
* field_types = {
* "workflow"
* },
* quickedit = {
* "editor" = "disabled"
* }
* )
*/
class WorkflowDefaultFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
/**
* The workflow storage.
*
* @var \Drupal\workflow\Entity\WorkflowStorage
*/
protected $storage;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The render controller.
*
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
protected $viewBuilder;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity form builder.
*
* @var \Drupal\Core\Entity\EntityFormBuilderInterface
*/
protected $entityFormBuilder;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('current_user'),
$container->get('entity.manager'),
$container->get('entity.form_builder')
);
}
/**
* Constructs a new WorkflowDefaultFormatter.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Third party settings.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
* The entity manager
* @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
* The entity form builder.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_manager, EntityFormBuilderInterface $entity_form_builder) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->viewBuilder = $entity_manager->getViewBuilder('workflow_transition');
$this->storage = $entity_manager->getStorage('workflow_transition');
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_manager;
$this->entityFormBuilder = $entity_form_builder;
}
/**
* {@inheritdoc}
*
* N.B. A large part of this function is taken from CommentDefaultFormatter.
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$output = [];
$field_name = $this->fieldDefinition->getName();
$entity = $items->getEntity();
$entity_type = $entity->getEntityTypeId();
$user = \Drupal::currentUser(); // @todo #2287057: OK?
// @todo: Perhaps global user is not always the correct user.
// E.g., on ScheduledTransition->execute()? But this function is mostly used in UI.
$current_sid = WorkflowManager::getCurrentStateId($entity, $field_name);
/* @var $current_state WorkflowState */
$current_state = WorkflowState::load($current_sid);
// First compose the current value with the normal formatter from list.module.
$elements = workflow_state_formatter($entity, $field_name, $current_sid);
// The state must not be deleted, or corrupted.
if (!$current_state) {
return $elements;
}
// Check permission, so that even with state change rights,
// the form can be suppressed from the entity view (#1893724).
$type_id = $current_state->getWorkflowId();
if (!\Drupal::currentUser()->hasPermission("access $type_id workflow_transition form")) {
return $elements;
}
// Workflows are added to the search results and search index by
// workflow_node_update_index() instead of by this formatter, so don't
// return anything if the view mode is search_index or search_result.
if (in_array($this->viewMode, ['search_result', 'search_index'])) {
return $elements;
}
if ($entity_type == 'comment') {
// No Workflow form allowed on a comment display.
// (Also, this avoids a lot of error messages.)
return $elements;
}
// Only build form if user has possible target state(s).
if (!$current_state->showWidget($entity, $field_name, $user, FALSE)) {
return $elements;
}
// Remove the default formatter. We are now building the widget.
$elements = [];
// BEGIN Copy from CommentDefaultFormatter
$elements['#cache']['contexts'][] = 'user.permissions';
// Add the WorkflowTransitionForm to the page.
$output['workflows'] = WorkflowManager::getWorkflowTransitionForm($entity, $field_name);
// Only show the add workflow form if the user has permission.
$elements['#cache']['contexts'][] = 'user.roles';
// Do not show the form for the print view mode.
$elements[] = $output + [
'#workflow_type' => $this->getFieldSetting('workflow_type'),
'#workflow_display_mode' => $this->getFieldSetting('default_mode'),
'workflows' => [],
];
// END Copy from CommentDefaultFormatter
return $elements;
}
/**
* Retrieves the entity form builder.
*
* @return \Drupal\Core\Entity\EntityFormBuilderInterface
* The entity form builder.
*/
protected function entityFormBuilder() {
if (!$this->entityFormBuilder) {
$this->entityFormBuilder = $this->container()->get('entity.form_builder');
}
return $this->entityFormBuilder;
}
}
@@ -0,0 +1,421 @@
<?php
namespace Drupal\workflow\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\Url;
use Drupal\options\Plugin\Field\FieldType\ListItemBase;
use Drupal\workflow\Entity\Workflow;
use Drupal\workflow\Entity\WorkflowState;
/**
* Plugin implementation of the 'workflow' field type.
*
* @FieldType(
* id = "workflow",
* label = @Translation("Workflow state"),
* description = @Translation("This field stores Workflow values for a certain Workflow type from a list of allowed 'value => label' pairs, i.e. 'Publishing': 1 => unpublished, 2 => draft, 3 => published."),
* category = @Translation("Workflow"),
* default_widget = "workflow_default",
* default_formatter = "list_default",
* constraints = {
* "WorkflowField" = {}
* },
* )
*/
class WorkflowItem extends ListItemBase {
//class WorkflowItem extends FieldItemBase implements OptionsProviderInterface {
// @todo D8-port: perhaps even:
//class WorkflowItem extends FieldStringItem {
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
$schema = [
'columns' => [
'value' => [
'description' => 'The {workflow_states}.sid that this entity is currently in.',
'type' => 'varchar',
'length' => 128,
],
],
'indexes' => [
'value' => ['value'],
],
];
return $schema;
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
/**
* Property definitions of the contained properties.
*
* @see FileItem::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
$definition['settings']['target_type'] = 'workflow_transition';
// Definitions vary by entity type and bundle, so key them accordingly.
$key = $definition['settings']['target_type'] . ':';
$key .= isset($definition['settings']['target_bundle']) ? $definition['settings']['target_bundle'] : '';
if (!isset($propertyDefinitions[$key])) {
$propertyDefinitions[$key]['value'] = DataDefinition::create('string') // TODO D8-port : or 'any'
->setLabel(t('Workflow state'))
->addConstraint('Length', ['max' => 128])
->setRequired(TRUE);
//workflow_debug( __FILE__ , __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
/*
// @todo D8-port: test this.
$propertyDefinitions[$key]['workflow_transition'] = DataDefinition::create('any')
// $properties['workflow_transition'] = DataDefinition::create('WorkflowTransition')
->setLabel(t('Transition'))
->setDescription(t('The computed WorkflowItem object.'))
->setComputed(TRUE)
->setClass('\Drupal\workflow\Entity\WorkflowTransition')
->setSetting('date source', 'value');
$propertyDefinitions[$key]['display'] = array(
'type' => 'boolean',
'label' => t('Flag to control whether this file should be displayed when viewing content.'),
);
$propertyDefinitions[$key]['description'] = array(
'type' => 'string',
'label' => t('A description of the file.'),
);
$propertyDefinitions[$key]['display'] = array(
'type' => 'boolean',
'label' => t('Flag to control whether this file should be displayed when viewing content.'),
);
$propertyDefinitions[$key]['description'] = array(
'type' => 'string',
'label' => t('A description of the file.'),
);
*/
}
return $propertyDefinitions[$key];
}
/**
* {@inheritdoc}
*/
public function getEntity() {
$entity = parent::getEntity();
// For Workflow on CommentForm, get the CommentedEntity.
if ($entity->getEntityTypeId() == 'comment') {
/** @var \Drupal\comment\CommentInterface $entity */
$entity = $entity->getCommentedEntity();
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$is_empty = empty($this->value);
return $is_empty;
}
/**
* {@inheritdoc}
*/
public function onChange($property_name, $notify = TRUE) {
//workflow_debug( __FILE__ , __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
// TODO D8: use this function onChange for adding a line in table workflow_transition_*
// Enforce that the computed date is recalculated.
//if ($property_name == 'value') {
// $this->date = NULL;
//}
parent::onChange($property_name, $notify);
}
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return [
'workflow_type' => '',
'allowed_values_function' => 'workflow_state_allowed_values',
] + parent::defaultStorageSettings();
}
/**
* Implements hook_field_settings_form() -> ConfigFieldItemInterface::settingsForm().
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$element = [];
// Create list of all Workflow types. Include an initial empty value.
// Validate each workflow, and generate a message if not complete.
$workflows = workflow_get_workflow_names(FALSE);
// @todo D8: add this to WorkflowFieldConstraintValidator.
// Set message, if no 'validated' workflows exist.
if (count($workflows) == 1) {
drupal_set_message(
$this->t('You must <a href=":create">create at least one workflow</a>
before content can be assigned to a workflow.',
[':create' => Url::fromRoute('entity.workflow_type.collection')->toString(),]
), 'warning'
);
}
// Validate via annotation WorkflowFieldConstraint. Show a message for each error.
$violation_list = $this->validate();
/** @var \Symfony\Component\Validator\ConstraintViolation $violation */
foreach ($violation_list->getIterator() as $violation) {
switch ($violation->getPropertyPath()) {
case 'fieldnameOnComment':
// @todo D8: CommentForm & constraints on storageSettingsForm()
// A 'comment' field name MUST be equal to content field name.
// @todo: Still not waterproof. You could have a field on a non-relevant entity_type.
drupal_set_message($violation->getMessage(), 'error');
$workflows = [];
break;
default:
break;
}
}
// @todo D8: CommentForm & constraints on storageSettingsForm
// Set the required workflow_type on 'comment' fields.
// N.B. the following must BELOW the (count($workflows) == 1) snippet.
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage */
$field_storage = $this->getFieldDefinition()->getFieldStorageDefinition();
if (!$this->getSetting('workflow_type') && $field_storage->getTargetEntityTypeId() == 'comment') {
$field_name = $field_storage->get('field_name');
$workflows = [];
foreach (_workflow_info_fields($entity = NULL, $entity_type = '', $entity_bundle = '', $field_name) as $key => $info) {
if ($info->getName() == $field_name && ($info->getTargetEntityTypeId() !== 'comment')) {
$wid = $info->getSetting('workflow_type');
$workflow = Workflow::load($wid);
$workflows[$wid] = $workflow->label();
}
}
}
// Let the user choose between the available workflow types.
$wid = $this->getSetting('workflow_type');
$url = Url::fromRoute('entity.workflow_type.collection')->toString();
$element['workflow_type'] = [
'#type' => 'select',
'#title' => $this->t('Workflow type'),
'#options' => $workflows,
'#default_value' => $wid,
'#required' => TRUE,
'#disabled' => $has_data,
'#description' => $this->t('Choose the Workflow type. Maintain workflows
<a href=":url">here</a>.', [':url' => $url]),
];
// Get a string representation to show all options.
/*
* Overwrite ListItemBase::storageSettingsForm().
*/
if ($wid) {
$allowed_values = WorkflowState::loadMultiple([], $wid);
// $allowed_values_function = $this->getSetting('allowed_values_function');
$element['allowed_values'] = [
'#type' => 'textarea',
'#title' => $this->t('Allowed values for the selected Workflow type'),
'#default_value' => ($wid) ? $this->allowedValuesString($allowed_values) : [],
'#rows' => count($allowed_values),
'#access' => ($wid) ? TRUE : FALSE, // User can see the data,
'#disabled' => TRUE, // .. but cannot change them.
'#element_validate' => [[get_class($this), 'validateAllowedValues']],
'#field_has_data' => $has_data,
'#field_name' => $this->getFieldDefinition()->getName(),
'#entity_type' => ($this->getEntity()) ? $this->getEntity()->getEntityTypeId() : '',
'#allowed_values' => $allowed_values,
'#description' => $this->allowedValuesDescription(),
];
}
return $element;
}
/**
* {@inheritdoc}
*/
protected function allowedValuesDescription() {
return '';
}
/**
* Generates a string representation of an array of 'allowed values'.
*
* This string format is suitable for edition in a textarea.
*
* @param WorkflowState[] $states
* An array of WorkflowStates, where array keys are values and array values are
* labels.
*
* @return string
* The string representation of the $states array:
* - Values are separated by a carriage return.
* - Each value is in the format "value|label" or "value".
*/
protected function allowedValuesString($states) {
$lines = [];
$wid = $this->getSetting('workflow_type');
$previous_wid = -1;
/** @var \Drupal\workflow\Entity\WorkflowState $state */
foreach ($states as $key => $state) {
// Only show enabled states.
if ($state->isActive()) {
// Show a Workflow name between Workflows, if more then 1 in the list.
if ((!$wid) && ($previous_wid <> $state->getWorkflowId())) {
$previous_wid = $state->getWorkflowId();
$lines[] = $state->getWorkflow()->label() . "'s states: ";
}
$label = $this->t('@label', ['@label' => $state->label()]);
$lines[] = " $key|$label";
}
}
return implode("\n", $lines);
}
// /**
// * {@inheritdoc}
// */
// public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
// // @todo Implement this once https://www.drupal.org/node/2238085 lands.
// $values['value'] = rand(pow(10, 8), pow(10, 9)-1);
// return $values;
// }
/**
* Implementation of TypedDataInterface
*
* @see folder \workflow\src\Plugin\Validation\Constraint
*/
/**
* {@inheritdoc}
*
* @see folder \workflow\src\Plugin\Validation\Constraint
*/
// public function getConstraints() {
// $constraints = parent::getConstraints();
// return $constraints;
// }
/**
* {@inheritdoc}
*
* @see folder \workflow\src\Plugin\Validation\Constraint
*/
// public function validate() {
// $result = parent::validate();
// return $result;
// }
/**
* Implementation of OptionsProviderInterface
*
* An array of settable options for the object that may be used in an
* Options widget, usually when new data should be entered. It may either be
* a flat array of option labels keyed by values, or a two-dimensional array
* of option groups (array of flat option arrays, keyed by option group
* label). Note that labels should NOT be sanitized.
*/
/**
* {@inheritdoc}
*/
public function getPossibleValues(AccountInterface $account = NULL) {
// Flatten options first, because SettableOptions may have 'group' arrays.
$flatten_options = OptGroup::flattenOptions($this->getPossibleOptions($account));
return array_keys($flatten_options);
}
/**
* {@inheritdoc}
*/
public function getPossibleOptions(AccountInterface $account = NULL) {
$allowed_options = [];
// When we are initially on the Storage settings form, no wid is set, yet.
if (!$wid = $this->getSetting('workflow_type')) {
return $allowed_options;
}
// Create an empty State. This triggers to show all possible states for the Workflow.
if ($workflow = Workflow::load($wid)) {
// There is no entity, E.g., on the Rules action "Set a data value".
$user = workflow_current_user($account); // @todo #2287057: OK?
/** @var \Drupal\workflow\Entity\WorkflowState $state */
$state = WorkflowState::create(['wid' => $wid, 'id' => '']);
$allowed_options = $state->getOptions(NULL, '', $user, FALSE);
}
return $allowed_options;
}
/**
* {@inheritdoc}
*/
public function getSettableValues(AccountInterface $account = NULL) {
// Flatten options first, because SettableOptions may have 'group' arrays.
$flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account));
return array_keys($flatten_options);
}
/**
* {@inheritdoc}
*/
public function getSettableOptions(AccountInterface $account = NULL) {
$allowed_options = [];
// When we are initially on the Storage settings form, no wid is set, yet.
if (!$wid = $this->getSetting('workflow_type')) {
return $allowed_options;
}
// Use the 'allowed_values_function' to calculate the options.
$definition = $this->getFieldDefinition()->getFieldStorageDefinition();
$entity = $this->getEntity();
$field_name = $definition->getName();
$user = workflow_current_user($account); // @todo #2287057: OK?
// Get the allowed new states for the entity's current state.
// @todo D8-port: overwrite getValue().
// $sid = $value = $this->getValue();
/** @var \Drupal\workflow\Entity\WorkflowState $state */
$sid = workflow_node_current_state($entity, $field_name);
$state = WorkflowState::load($sid);
$allowed_options = ($state) ? $state->getOptions($entity, $field_name, $user, FALSE) : [];
return $allowed_options;
}
}
@@ -0,0 +1,233 @@
<?php
namespace Drupal\workflow\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflow\Element\WorkflowTransitionElement;
use Drupal\workflow\Entity\Workflow;
use Drupal\workflow\Entity\WorkflowManager;
use Drupal\workflow\Entity\WorkflowTransition;
/**
* Plugin implementation of the 'workflow_default' widget.
*
* @FieldWidget(
* id = "workflow_default",
* label = @Translation("Workflow transition form"),
* field_types = {
* "workflow"
* },
* )
*/
class WorkflowDefaultWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
/*
'workflow_default' => array(
'label' => t('Workflow'),
'field types' => array('workflow'),
'settings' => array(
'fieldset' => 0,
'name_as_title' => 1,
'comment' => 1,
),
),
*/
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*
* Be careful: Widget may be shown in very different places. Test carefully!!
* - On a entity add/edit page
* - On a entity preview page
* - On a entity view page
* - On a entity 'workflow history' tab
* - On a comment display, in the comment history
* - On a comment form, below the comment history
*
* @todo D8: change "array $items" to "FieldInterface $items"
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$wid = $this->getFieldSetting('workflow_type');
/** @var $workflow Workflow */
if (!$workflow = Workflow::load($wid)) {
// @todo: add error message.
return $element;
}
if ($this->isDefaultValueWidget($form_state)) {
// On the Field settings page, User may not set a default value
// (this is done by the Workflow module).
return [];
}
/** @var $item \Drupal\workflow\Plugin\Field\FieldType\WorkflowItem */
$item = $items[$delta];
/** @var \Drupal\field\Entity\FieldConfig $field_config */
$field_config = $item->getFieldDefinition();
/** @var $field_storage \Drupal\field\Entity\FieldStorageConfig */
$field_storage = $field_config->getFieldStorageDefinition();
$entity = $item->getEntity();
$field_name = $field_storage->get('field_name');
// Create a transition, to pass to the form. No need to use setValues().
$from_sid = workflow_node_current_state($entity, $field_name);
/** @var $transition WorkflowTransition */
$transition = WorkflowTransition::create([$from_sid, 'field_name' => $field_name]);
$transition->setTargetEntity($entity);
// Here, on entity form, not the $element is added, but the entity form.
// Problem 1: adding the element, does not add added fields.
// Problem 2: adding the form, generates wrong UI.
// Problem 3: does not work on ScheduledTransition.
// Step 1: use the Element.
$element['#default_value'] = $transition;
$element += WorkflowTransitionElement::transitionElement($element, $form_state, $form);
// Step 2: use the Form, in order to get extra fields.
$workflow_form = WorkflowManager::getWorkflowTransitionForm($entity, $field_name);
// Determine and add the attached fields.
$attached_fields = WorkflowManager::getAttachedFields('workflow_transition', $wid);
foreach ($attached_fields as $key => $attached_field) {
$element[$key] = $workflow_form[$key];
}
// Option 3: use the true Element.
// $form = $this->element($form, $form_state, $transition);
//$element['workflow_transition'] = array(
// '#type' => 'workflow_transition',
// '#title' => t('Workflow transition'),
// '#default_value' => $transition,
// );
return $element;
}
/**
* {@inheritdoc}
*
* Implements workflow_transition() -> WorkflowDefaultWidget::submit().
*
* Overrides submit(array $form, array &$form_state).
* Contains 2 extra parameters for D7
*
* @param array $form
* @param array $form_state
* @param array $items
* The value of the field.
* @param bool $force
* TRUE if all access must be overridden, e.g., for Rules.
*
* @return int
* If update succeeded, the new State Id. Else, the old Id is returned.
*
* This is called from function _workflow_form_submit($form, &$form_state)
* It is a replacement of function workflow_transition($entity, $to_sid, $force, $field)
* It performs the following actions;
* - save a scheduled action
* - update history
* - restore the normal $items for the field.
* @todo: remove update of {node_form} table. (separate task, because it has features, too)
*/
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
$user = workflow_current_user(); // @todo #2287057: verify if submit() really is only used for UI. If not, $user must be passed.
// Set the new value.
// Beware: We presume cardinality = 1 !!
// The widget form element type has transformed the value to a
// WorkflowTransition object at this point. We need to convert it
// back to the regular 'value' string format.
foreach ($values as &$item) {
if (!empty($item)) { // } && $item['value'] instanceof DrupalDateTime) {
// The following can NOT be retrieved from the WorkflowTransition.
/** @var $entity \Drupal\Core\Entity\EntityInterface */
$entity = $form_state->getFormObject()->getEntity();
/** @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */
$transition = $item['workflow_transition'];
// N.B. Use a proprietary version of copyFormValuesToEntity,
// where $entity/$transition is passed by reference.
/** @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */
$transition = WorkflowTransitionElement::copyFormValuesToTransition($transition, $form, $form_state, $item);
// Try to execute the transition. Return $from_sid when error.
if (!$transition) {
// This should not be possible (perhaps when testing/developing).
drupal_set_message(t('Error: the transition from %from_sid to %to_sid could not be generated.'), 'error');
// The current value is still the previous state.
$to_sid = $from_sid = 0;
}
else {
// The transition may be scheduled or not. Save the result, and
// rely upon hook workflow_entity_insert/update($entity) in
// file workflow.module to save/execute the transition.
// - validate option; add hook to let other modules change comment.
// - add to history; add to watchdog
// Return the new State ID. (Execution may fail and return the old Sid.)
// Get the new value from an action button if set in the workflow settings.
$action_info = _workflow_transition_form_get_triggering_button($form_state);
$field_name = $transition->getFieldName();
if ($field_name == $action_info['field_name']) {
$transition->set('to_sid', $action_info['to_sid']);
}
$force = FALSE; // @TODO D8-port: add to form for usage in VBO.
// Now, save/execute the transition.
$from_sid = $transition->getFromSid();
$force = $force || $transition->isForced();
if (!$transition->isAllowed($user, $force)) {
// Transition is not allowed.
$to_sid = $from_sid;
}
elseif (!$entity || !$entity->id()) {
// Entity is inserted. The Id is not yet known.
// So we can't yet save the transition right now, but must rely on
// function/hook workflow_entity_insert($entity) in file workflow.module.
// $to_sid = $transition->execute($force);
$to_sid = $transition->getToSid();
}
else {
// Entity is updated. To stay in sync with insert, we rely on
// function/hook workflow_entity_update($entity) in file workflow.module.
// $to_sid = $transition->execute($force);
$to_sid = $transition->getToSid();
}
}
// Now the data is captured in the Transition, and before calling the
// Execution, restore the default values for Workflow Field.
// For instance, workflow_rules evaluates this.
//
// N.B. Align the following functions:
// - WorkflowDefaultWidget::massageFormValues();
// - WorkflowManager::executeTransition().
// Set the transition back, to be used in hook_entity_update().
$item['workflow_transition'] = $transition;
// Set the value at the proper location.
if ($transition && $transition->isScheduled()) {
$item['value'] = $from_sid;
}
else {
$item['value'] = $to_sid;
}
}
}
return $values;
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\workflow\Plugin\Validation\Constraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
/**
* Supports validating Workflow Field names.
*
* @see https://drupalwatchdog.com/volume-5/issue-2/introducing-drupal-8s-entity-validation-api
*
* @Constraint(
* id = "WorkflowField",
* label = @Translation("Workflow field name", context = "Validation"),
* )
*/
class WorkflowFieldConstraint extends CompositeConstraintBase {
/**
* Message shown when a comment fieldname doesn't match an entity field name.
*
* @var string
*/
// @todo D8: CommentForm & constraints on Field
public $messageFieldname = 'A workflow field on a comment must have
the same field_name as the commented Entity. Please maintain the entity
first, or choose another field name.';
/**
* {@inheritdoc}
*/
public function coversFields() {
return ['field_name', ];
}
}
@@ -0,0 +1,92 @@
<?php
namespace Drupal\workflow\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\user\UserStorageInterface;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the CommentName constraint.
*
* @see https://drupalwatchdog.com/volume-5/issue-2/introducing-drupal-8s-entity-validation-api
*/
class WorkflowFieldConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* Validator 2.5 and upwards compatible execution context.
*
* @var \Symfony\Component\Validator\Context\ExecutionContextInterface
*/
protected $context;
/**
* User storage handler.
*
* @var \Drupal\user\UserStorageInterface
*/
protected $userStorage;
/**
* Constructs a new Validator.
*
* @param \Drupal\user\UserStorageInterface $user_storage
* The user storage handler.
*/
public function __construct(UserStorageInterface $user_storage) {
$this->userStorage = $user_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity.manager')->getStorage('user'));
}
/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
// Workflow field name on CommentForm has special requirements.
$field_storage = $entity->getFieldDefinition()->getFieldStorageDefinition();
if ($field_storage->getTargetEntityTypeId() == 'comment') {
if (!$this->isValidFieldname($field_storage)) {
$this->context->buildViolation($constraint->messageFieldname)
->atPath('fieldnameOnComment')
->addViolation();
}
}
}
/**
* @param FieldStorageConfig $field_storage
*
* @return bool
*/
protected function isValidFieldname(FieldStorageConfig $field_storage) {
$comment_field_name_ok = FALSE;
if ($field_storage->getTargetEntityTypeId() !== 'comment') {
return TRUE;
}
$field_name = $field_storage->get('field_name');
// Check if the 'comment' field name exists on the 'commented' entity type.
// @todo: Still not waterproof. You could have a field on a non-relevant entity_type.
foreach (_workflow_info_fields() as $key => $info) {
if (($info->getName() == $field_name) && ($info->getTargetEntityTypeId() !== 'comment')) {
$comment_field_name_ok = TRUE;
}
}
return $comment_field_name_ok;
}
}
@@ -0,0 +1,55 @@
<?php
namespace Drupal\workflow\Plugin\views\filter;
use Drupal\views\FieldAPIHandlerTrait;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\views\filter\ManyToOne;
use Drupal\views\ViewExecutable;
/**
* Filter handler which uses workflow_state as options.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("workflow_state")
*/
class WorkflowState extends ManyToOne {
use FieldAPIHandlerTrait;
/**
* {@inheritdoc}
*/
public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
parent::init($view, $display, $options);
$wid = isset($this->definition['wid']) ? $this->definition['wid'] : '';
$grouped = isset($options['group_info']['widget']) ? $options['group_info']['widget'] == 'select' : false;
$this->valueOptions = workflow_get_workflow_state_names($wid, $grouped);
}
/**
* Child classes should be used to override this function and set the
* 'value options', unless 'options callback' is defined as a valid function
* or static public method to generate these values.
*
* This can use a guard to be used to reduce database hits as much as
* possible.
*
* @return array|null
* The stored values from $this->valueOptions.
*/
public function getValueOptions() {
if (isset($this->valueOptions)) {
return $this->valueOptions;
}
//@todo: implement the below code, and remove the line from init.
// @todo: follow Options patterns
// @see callback_allowed_values_function()
// @see options_allowed_values()
return parent::getValueOptions();
}
}
@@ -0,0 +1,146 @@
<?php
namespace Drupal\workflow;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\workflow\Entity\WorkflowManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the access control handler for the workflow entity type.
*
* @see \Drupal\workflow\Entity\Workflow
* @ingroup workflow_access
*/
class WorkflowAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type
);
}
/**
* {@inheritdoc}
*/
public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = parent::access($entity, $operation, $account, TRUE);
$account = $this->prepareUser($account);
// This is only for Edit/Delete transition. For Add/create, use createAccess.
switch ($entity->getEntityTypeId()) {
case 'workflow_scheduled_transition':
switch ($operation) {
case 'update':
// This operation is not defined for Scheduled Transitions.
$result = AccessResult::forbidden();
break;
case 'delete':
// This operation is not defined for Scheduled Transitions.
$result = AccessResult::forbidden();
break;
case 'revert':
// This operation is not defined for Scheduled Transitions.
$result = AccessResult::forbidden();
break;
default:
$result = parent::access($entity, $operation, $account, $return_as_object);
break;
} // End of switch ($operation).
break;
case 'workflow_transition':
/* @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */
$transition = $entity;
switch ($operation) {
case 'update':
$is_owner = WorkflowManager::isOwner($account, $transition);
$type_id = $transition->getWorkflowId();
if ($account->hasPermission("bypass $type_id workflow_transition access")) {
$result = AccessResult::allowed()->cachePerPermissions();
}
elseif ($account->hasPermission("edit any $type_id workflow_transition")) {
$result = AccessResult::allowed()->cachePerPermissions();
}
elseif ($is_owner && $account->hasPermission("edit own $type_id workflow_transition")) {
$result = AccessResult::allowed()->cachePerPermissions();
}
return $return_as_object ? $result : $result->isAllowed();
case 'delete':
// The delete operation is not defined for Transitions.
$result = AccessResult::forbidden();
break;
case 'revert':
// @see workflow_operations.
default:
$result = parent::access($entity, $operation, $account, $return_as_object);
//if ($account->hasPermission("bypass $type_id workflow_transition access")) {
// $result = AccessResult::allowed()->cachePerPermissions();
//}
break;
} // End of switch ($operation).
break;
case 'workflow_config_transition':
// This is not (yet) configured.
break;
case 'workflow_state':
switch ($operation) {
case 'view label':
// The following two lines are copied from below, and need to be reviewed carefully.
$result = AccessResult::allowed();
return $return_as_object ? $result : $result->isAllowed();
default:
// E.g., operation 'update' on the WorkflowStates config page.
break;
} // End of switch ($operation).
break;
}
/** @var $result AccessResult $result */
// $result = parent::createAccess($entity_bundle, $account, $context, TRUE);
$result = $result->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
/** @var $result AccessResult $result */
$result = parent::createAccess($entity_bundle, $account, $context, TRUE);
$result = $result->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $transition, $operation, AccountInterface $account) {
return parent::checkAccess($transition, $operation, $account);
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet.
return AccessResult::allowedIf($account->hasPermission('create ' . $entity_bundle . ' content'))->cachePerPermissions();
}
}
@@ -0,0 +1,79 @@
<?php
namespace Drupal\workflow;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\workflow\Entity\Workflow;
/**
* Provides dynamic permissions for workflows of different types.
*/
class WorkflowPermissions {
use StringTranslationTrait;
/**
* Returns an array of workflow type permissions.
*
* @return array
* The workflow type permissions.
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
public function workflowTypePermissions() {
$perms = [];
// Generate workflow permissions for all workflow types.
foreach (Workflow::loadMultiple() as $type) {
$perms += $this->buildPermissions($type);
}
return $perms;
}
/**
* Returns a list of workflow permissions for a given workflow type.
*
* @param \Drupal\workflow\Entity\Workflow $type
* The workflow type.
*
* @return array
* An associative array of permission names and descriptions.
*/
protected function buildPermissions(Workflow $type) {
$type_id = $type->id();
$type_params = ['%type_name' => $type->label()];
return [
// D7->D8-Conversion of the 'User 1 is special' permission (@see NodePermissions::bypass node access).
"bypass $type_id workflow_transition access" => [
'title' => $this->t('%type_name: Bypass transition access control', $type_params),
'description' => $this->t('View, edit and delete all transitions regardless of permission restrictions.'),
'restrict access' => TRUE,
],
// D7->D8-Conversion of 'participate in workflow' permission to "create $type_id transition" (@see NodePermissions::create content).
"create $type_id workflow_transition" => [
'title' => $this->t('%type_name: Participate in workflow', $type_params),
'description' => $this->t("Role is enabled to create state transitions. (Determines transition-specific permission on the workflow admin page.
<i> Warning: For better control, uncheck the 'Authenticated user', and manage the permissions per separate role.</i>)"),
],
// D7->D8-Conversion of 'schedule workflow transitions' permission to "schedule $type_id transition" (@see NodePermissions::create content).
"schedule $type_id workflow_transition" => [
'title' => $this->t('%type_name: Schedule state transition', $type_params),
'description' => $this->t('Role is enabled to schedule state transitions.'),
],
// D7->D8-Conversion of 'workflow history' permission on Workflow settings to "access $type_id overview" (@see NodePermissions::access content overview).
"access own $type_id workflow_transion overview" => [
'title' => $this->t('%type_name: Access Workflow history tab of own content', $type_params),
'description' => $this->t('Role is enabled to view the "Workflow state transition history" tab on own entity.'),
],
"access any $type_id workflow_transion overview" => [
'title' => $this->t('%type_name: Access Workflow history tab of any content', $type_params),
'description' => $this->t('Role is enabled to view the "Workflow state transition history" tab on any entity.'),
],
// D7->D8-Conversion of 'show workflow transition form' permission. @see #1893724
"access $type_id workflow_transition form" => [
'title' => $this->t('%type_name: Access the Workflow state transition form on entity view page', $type_params),
'description' => $this->t('Role is enabled to view a "Workflow state transition" block/widget and add a state transition on the entity page.'),
],
];
}
}
@@ -0,0 +1,22 @@
<?php
namespace Drupal\workflow;
use Drupal\views\EntityViewsData;
/**
* Provides the views data for the workflow entity type.
*/
class WorkflowScheduledTransitionViewsData extends WorkflowTransitionViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
// @todo D8-port: Add some data from D7 function workflow_views_views_data_alter() ??
// @see http://cgit.drupalcode.org/workflow/tree/workflow_views/workflow_views.views.inc
return $data;
}
}
@@ -0,0 +1,262 @@
<?php
namespace Drupal\workflow;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\workflow\Entity\WorkflowTransition;
/**
* Defines a class to build a draggable listing of Workflow State entities.
*
* @deprecated use View 'Workflow Entity history' in WorkflowTransitionListController.
* @see \Drupal\workflow\Entity\WorkflowState
*/
class WorkflowTransitionListBuilder extends EntityListBuilder {
const WORKFLOW_MARK_STATE_IS_DELETED = '*';
/**
* A variable to pass the entity of a transition to the ListBuilder.
* @todo: There should be a better way.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
public $workflow_entity;
/**
* {@inheritdoc}
*/
protected $limit = 50;
/**
* Indicates if a column 'Field name' must be generated.
*
* @var bool
*/
protected $show_column_fieldname = NULL;
/**
* Indicates if a footer must be generated.
*
* @var bool
*/
protected $footer_needed = FALSE;
/**
* {@inheritdoc}
*/
public function load() {
/** @var $entity \Drupal\Core\Entity\EntityInterface */
$entity = $this->workflow_entity; // N.B. This is a custom variable.
$entity_type = $entity->getEntityTypeId();
$entity_id = $entity->id();
$field_name = workflow_url_get_field_name();
// @todo D8-port: document $limit. Should be used in pager, not in load().
$this->limit = \Drupal::config('workflow.settings')->get('workflow_states_per_page');
$limit = $this->limit;
// Get Transitions with highest timestamp first.
$entities = WorkflowTransition::loadMultipleByProperties($entity_type, [$entity_id], [], $field_name, '', $limit, 'DESC');
return $entities;
}
/**
* {@inheritdoc}
*
* Building the header and content lines for the contact list.
*
* Calling the parent::buildHeader() adds a column for the possible actions
* and inserts the 'edit' and 'delete' links as defined for the entity type.
*/
public function buildHeader() {
$entity = $this->workflow_entity; // N.B. This is a custom variable.
$header['timestamp'] = $this->t('Date');
if ($this->showColumnFieldname($entity)) {
$header['field_name'] = $this->t('Field name');
}
$header['from_state'] = $this->t('From State');
$header['to_state'] = $this->t('To State');
$header['user_name'] = $this->t('By');
$header['comment'] = $this->t('Comment');
// column 'Operations' is now added by core.
//$header['operations'] = $this->t('Operations');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $transition) {
// Show the history table.
$current_themed = FALSE;
/** @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */
$entity = $transition->getTargetEntity();
$field_name = $transition->getFieldName();
$current_sid = workflow_node_current_state($entity, $field_name);
$to_state = $transition->getToState();
if (!$to_state) {
// This is an invalid/deleted state.
$to_label = self::WORKFLOW_MARK_STATE_IS_DELETED;
// Add a footer to explain the addition.
$this->footer_needed = TRUE;
}
else {
$label = Html::escape($this->t($to_state->label()));
if ($transition->getToSid() == $current_sid && $to_state->isActive() && !$current_themed) {
$to_label = $label;
// Make a note that we have themed the current state; other times in the history
// of this entity where the entity was in this state do not need to be specially themed.
$current_themed = TRUE;
}
elseif (!$to_state->isActive()) {
$to_label = $label . self::WORKFLOW_MARK_STATE_IS_DELETED;
// Add a footer to explain the addition.
$this->footer_needed = TRUE;
}
else {
// Regular state.
$to_label = $label;
}
}
unset($to_state); // Not needed anymore.
$from_state = $transition->getFromState();
if (!$from_state) {
// This is an invalid/deleted state.
$from_label = self::WORKFLOW_MARK_STATE_IS_DELETED;
// Add a footer to explain the addition.
$this->footer_needed = TRUE;
}
else {
$label = Html::escape($this->t($from_state->label()));
if (!$from_state->isActive()) {
$from_label = $label . self::WORKFLOW_MARK_STATE_IS_DELETED;
// Add a footer to explain the addition.
$this->footer_needed = TRUE;
}
else {
// Regular state.
$from_label = $label;
}
}
unset($from_state); // Not needed anymore.
$owner = $transition->getOwner();
$field_label = $transition->getFieldName();
$variables = [
'transition' => $transition,
'extra' => '',
'from_label' => $from_label,
'to_label' => $to_label,
'user' => $owner,
];
// Allow other modules to modify the row.
\Drupal::moduleHandler()->alter('workflow_history', $variables);
// 'class' => array('workflow_history_row'), // TODO D8-port
$row['timestamp']['data'] = $transition->getTimestampFormatted(); // 'class' => array('timestamp')
// html_entity_decode() transforms chars like '&' correctly.
if ($this->showColumnFieldname($entity)) {
$row['field_name']['data'] = html_entity_decode($field_label);
}
$row['from_state']['data'] = html_entity_decode($from_label); // 'class' => array('previous-state-name'))
$row['to_state']['data'] = html_entity_decode($to_label); // 'class' => array('state-name'))
$row['user_name']['data'] = $owner->toLink($owner->getDisplayName())->toString(); // 'class' => array('user-name')
$row['comment']['data'] = html_entity_decode($transition->getComment()); // 'class' => array('log-comment')
// Column 'Operations' is now added by core.
// D7: $row['operations']['data'] = $this->buildOperations($entity);
$row += parent::buildRow($transition);
return $row;
}
/**
* {@inheritdoc}
*
* Builds the entity listing as renderable array for table.html.twig.
*/
public function render() {
$build = [];
// @todo d8-port: get pager working.
$this->limit = \Drupal::config('workflow.settings')->get('workflow_states_per_page'); // TODO D8-port
// $output .= theme('pager', array('tags' => $limit)); // @todo D8-port
$build += parent::render();
// Add a footer. This is not yet added in EntityListBuilder::render()
if ($this->footer_needed) { // @todo D8-port: test this.
// Two variants. First variant is official, but I like 2nd better.
/*
$build['table']['#footer'] = [
[
'class' => ['footer-class'],
'data' => [
[
'data' => WORKFLOW_MARK_STATE_IS_DELETED . ' ' . t('State is no longer available.'),
'colspan' => count($build['table']['#header']),
],
],
],
];
*/
$build['workflow_footer'] = [
'#markup' => WORKFLOW_MARK_STATE_IS_DELETED . ' ' . t('State is no longer available.'),
'#weight' => 500, // @todo Make this better.
];
}
return $build;
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
if (isset($operations['edit'])) {
$destination = \Drupal::destination()->getAsArray();
$operations['edit']['query'] = $destination;
}
return $operations;
}
/**
* Gets the title of the page.
*
* @return string
* A string title of the page.
*/
protected function getTitle() {
return $this->t('Workflow history');
}
/**
* Determines if the column 'Field name' must be shown.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*
* @return bool
*/
protected function showColumnFieldname(EntityInterface $entity) {
if (is_null($this->show_column_fieldname)) {
// @todo: also remove when field_name is set in route??
if (count(_workflow_info_fields($entity)) > 1) {
$this->show_column_fieldname = TRUE;
}
}
return $this->show_column_fieldname;
}
}
@@ -0,0 +1,176 @@
<?php
namespace Drupal\workflow;
use Drupal\views\EntityViewsData;
/**
* Provides the views data for the workflow entity type.
* Partly taken from NodeViewsData.php.
*/
class WorkflowTransitionViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
// Use flexible $base_table, since both WorkflowTransition and
// WorkflowScheduledTransition use this.
$base_table = $this->entityType->getBaseTable();
$base_field = $this->entityType->getKey('id');
// @todo D8-port: Add data from D7 function workflow_views_views_data_alter()
// @see http://cgit.drupalcode.org/workflow/tree/workflow_views/workflow_views.views.inc
$data[$base_table]['table']['group'] = $this->t('Workflow');
$data[$base_table]['table']['provider'] = 'workflow';
$data[$base_table]['table']['join'] = [
// This is provided for the many_to_one argument.
$base_table => [
'field' => $base_field,
'left_field' => $base_field,
],
];
// @todo: this relationship needs to be reversed. See also taxonomy/src/NodeTermData.php
//$data[$base_table]['nid'] = [
// 'title' => $this->t('Content with workflow'),
// 'help' => $this->t('Relate all content with a workflow.'),
// 'relationship' => [
// 'id' => 'standard',
// 'base' => 'node',
// 'base field' => 'nid',
// 'label' => $this->t('node'),
// 'skip base' => 'node',
// ],
//];
$data[$base_table]['from_sid']['filter']['id'] = 'workflow_state';
$data[$base_table]['from_sid']['help'] = $this->t('The name of the previous state of the transition.');
$data[$base_table]['to_sid']['filter']['id'] = 'workflow_state';
$data[$base_table]['to_sid']['help'] = $this->t('The name of the new state of the transition. (For the latest transition, this is the current state.)');
$data[$base_table]['uid']['help'] = $this->t('The user who triggered the transition. If you need more fields than the uid add the content: author relationship');
$data[$base_table]['uid']['filter']['id'] = 'user_name';
$data[$base_table]['uid']['relationship']['title'] = $this->t('User');
$data[$base_table]['uid']['relationship']['help'] = $this->t('The user who triggered the transition.');
$data[$base_table]['uid']['relationship']['label'] = $this->t('User');
// @todo Add similar support to any date field
// @see https://www.drupal.org/node/2337507
$data[$base_table]['timestamp_fulldate'] = [
'title' => $this->t('Created date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'timestamp',
'id' => 'date_fulldate',
],
];
$data[$base_table]['timestamp_year_month'] = [
'title' => $this->t('Created year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'timestamp',
'id' => 'date_year_month',
],
];
$data[$base_table]['timestamp_year'] = [
'title' => $this->t('Created year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'timestamp',
'id' => 'date_year',
],
];
$data[$base_table]['timestamp_month'] = [
'title' => $this->t('Created month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'timestamp',
'id' => 'date_month',
],
];
$data[$base_table]['timestamp_day'] = [
'title' => $this->t('Created day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'timestamp',
'id' => 'date_day',
],
];
$data[$base_table]['timestamp_week'] = [
'title' => $this->t('Created week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'timestamp',
'id' => 'date_week',
],
];
$data[$base_table]['changed_fulldate'] = [
'title' => $this->t('Updated date'),
'help' => $this->t('Date in the form of CCYYMMDD.'),
'argument' => [
'field' => 'changed',
'id' => 'date_fulldate',
],
];
$data[$base_table]['changed_year_month'] = [
'title' => $this->t('Updated year + month'),
'help' => $this->t('Date in the form of YYYYMM.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year_month',
],
];
$data[$base_table]['changed_year'] = [
'title' => $this->t('Updated year'),
'help' => $this->t('Date in the form of YYYY.'),
'argument' => [
'field' => 'changed',
'id' => 'date_year',
],
];
$data[$base_table]['changed_month'] = [
'title' => $this->t('Updated month'),
'help' => $this->t('Date in the form of MM (01 - 12).'),
'argument' => [
'field' => 'changed',
'id' => 'date_month',
],
];
$data[$base_table]['changed_day'] = [
'title' => $this->t('Updated day'),
'help' => $this->t('Date in the form of DD (01 - 31).'),
'argument' => [
'field' => 'changed',
'id' => 'date_day',
],
];
$data[$base_table]['changed_week'] = [
'title' => $this->t('Updated week'),
'help' => $this->t('Date in the form of WW (01 - 53).'),
'argument' => [
'field' => 'changed',
'id' => 'date_week',
],
];
return $data;
}
}
@@ -0,0 +1,431 @@
<?php
/**
* @file
* Hooks provided by the workflow module.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\UserInterface;
use Drupal\workflow\Entity\Workflow;
use Drupal\workflow\Entity\WorkflowConfigTransition;
use Drupal\workflow\Entity\WorkflowState;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
/**
* Implements hook_workflow_operations().
*
* Adds extra operations to ListBuilders.
* - workflow_ui: Workflow, State;
* - workflow: WorkflowTransition;
*
* @param string $op
* 'top_actions': Allow modules to insert their own front page action links.
* 'operations': Allow modules to insert their own workflow operations.
* 'workflow': Allow modules to insert workflow operations.
* 'state': Allow modules to insert state operations.
* @param EntityInterface|null $entity
* The current workflow/state/transition object.
*
* @return array
* The new actions, to be added to the entity list.
*/
function hook_workflow_operations($op, EntityInterface $entity = NULL) {
$operations = [];
switch ($op) {
case 'top_actions':
// As of D8, below hook_workflow_operations is removed, in favour of core hooks.
// @see file workflow_ui.links.action.yml for an example top action.
return $operations;
case 'operations':
break;
case 'workflow':
// This example adds an operation to the 'operations column' of the Workflow List.
/* @var $workflow Workflow */
$workflow = $entity;
$alt = t('Control content access for @wf', ['@wf' => $workflow->label()]);
$attributes = ['alt' => $alt, 'title' => $alt];
$operations['workflow_access_form'] = [
'title' => t('Access'),
'weight' => 50,
'url' => \Drupal\Core\Url::fromRoute('entity.workflow_type.access_form', ['workflow_type' => $workflow->id()]),
'query' => \Drupal::destination()->getAsArray(), // Add destination.
];
return $operations;
case 'state':
/* @var $state WorkflowState */
$state = $entity;
break;
case 'workflow_transition':
// As of D8, below hook_workflow_operations is removed, in favour of core hooks.
// @see EntityListBuilder::getOperations, workflow_operations, workflow.api.php.
// Your module may add operations to the Entity list.
/* @var $transition WorkflowTransitionInterface */
$transition = $entity;
break;
default:
break;
}
return $operations;
}
/**
* Implements hook_workflow().
*
* NOTE: This hook may reside in the implementing module
* or in a module.workflow.inc file.
*
* @param string $op
* The current workflow operation.
* E.g., 'transition pre', 'transition post'.
* @param WorkflowTransitionInterface $transition
* The transition, that contains all of the above.
* @param UserInterface $user
*
* @return bool|void
*/
function hook_workflow($op, WorkflowTransitionInterface $transition, UserInterface $user) {
switch ($op) {
case 'transition permitted':
// As of version 8.x-1.x, this operation is never called to check if transition is permitted.
// This was called in the following situations:
// case 1. when building a workflow widget with list of available transitions;
// case 2. when executing a transition, just before the 'transition pre';
// case 3. when showing a 'revert state' link in a Views display.
// Your module's implementation may return FALSE here and disallow
// the execution, or avoid the presentation of the new State.
// This may be user-dependent.
// As of version 8.x-1.x:
// case 1: use hook_workflow_permitted_state_transitions_alter();
// case 2: use the 'transition pre' operation;
// case 3: use the 'transition pre' operation;
return TRUE;
case 'transition revert':
// Hook is called when showing the Transition Revert form.
// Implement this hook if you need to control this.
// If you return FALSE here, you will veto the transition.
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, $op, '');
break;
case 'transition pre':
// The workflow module does nothing during this operation.
// Implement this hook if you need to change/do something BEFORE anything
// is saved to the database.
// If you return FALSE here, you will veto the transition.
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, $op, '');
break;
case 'transition post':
// In D7, this is called by Workflow Node during update of the state, directly
// after updating the Workflow. Workflow Field does not call this,
// since you can call a hook_entity_* event after saving the entity.
// @see https://api.drupal.org/api/drupal/includes%21module.inc/group/hooks/7
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, $op, '');
break;
case 'transition delete':
case 'state delete':
case 'workflow delete':
// These hooks are removed in D8, in favour of the core hooks:
// - workflow_entity_predelete(EntityInterface $entity)
// - workflow_entity_delete(EntityInterface $entity)
// See examples at the bottom of this file.
break;
}
return;
}
/**
* Implements hook_workflow_history_alter().
*
* In D8, hook_workflow_history_alter() is removed, in favour
* of ListBuilder::getDefaultOperations
* and hook_workflow_operations('workflow_transition').
*
* Allow other modules to add Operations to the most recent history change.
* E.g., Workflow Revert implements an 'undo' operation.
*
* @param array $variables
* The current workflow history information as an array.
* 'old_sid' - The state ID of the previous state.
* 'old_state_name' - The state name of the previous state.
* 'sid' - The state ID of the current state.
* 'state_name' - The state name of the current state.
* 'history' - The row from the workflow_transition_history table.
* 'transition' - a WorkflowTransition object, containing all of the above.
*/
function hook_workflow_history_alter(array &$variables) {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', '');
// The Workflow module does nothing with this hook.
// For an example implementation, see the Workflow Revert add-on.
}
/**
* Implements hook_workflow_comment_alter().
*
* Allow other modules to change the user comment when saving a state change.
*
* @param string $comment
* The comment of the current state transition.
* @param array $context
* 'transition' - The current transition itself.
*/
function hook_workflow_comment_alter(&$comment, array &$context) {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', '');
/* @var $transition WorkflowTransitionInterface */
$transition = $context['transition'];
//$comment = $transition->getOwner()->getUsername() . ' says: ' . $comment;
}
/**
* Implements hook_workflow_permitted_state_transitions_alter().
*
* @param array $transitions
* An array of allowed transitions from the current state (as provided in
* $context). They are already filtered by the settings in Admin UI.
* @param array $context
* An array of relevant objects. Currently:
* $context = array(
* 'user' => $user,
* 'workflow' => $workflow,
* 'state' => $current_state,
* 'force' => $force,
* );
*
* This hook allows you to add custom filtering of allowed target states, add
* new custom states, change labels, etc.
* It is invoked in WorkflowState::getOptions().
*/
function hook_workflow_permitted_state_transitions_alter(array &$transitions, array $context) {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', '');
$user = $context['user']; // user may have the custom role AUTHOR.
// The following could be fetched from each transition.
$workflow = $context['workflow'];
$current_state = $context['state'];
// The following could be fetched from the $user and $transition objects.
$force = $context['force'];
// Implement here own permission logic.
foreach ($transitions as $key => $transition) {
/* @var $transition WorkflowTransitionInterface */
if (!$transition->isAllowed($user, $force)) {
//unset($transitions[$key]);
}
}
// This example creates a new custom target state.
$values = [
// Fixed values for new transition.
'wid' => $context['workflow']->id(),
'from_sid' => $context['state']->id(),
// Custom values for new transition.
// The ID must be an integer, due to db-table constraints.
'to_sid' => '998',
'label' => 'go to my new fantasy state',
];
$new_transition = WorkflowConfigTransition::create($values);
// $transitions[] = $new_transition;
}
/**********************************************************************
* Hooks defined by core Form API: hooks to to alter the Workflow Form/Widget.
*/
/**
* Alter forms for field widgets provided by other modules.
*
* @param $element
* The field widget form element as constructed by hook_field_widget_form().
* @param $form_state
* An associative array containing the current state of the form.
* @param $context
* An associative array containing the following key-value pairs, matching the
* arguments received by hook_field_widget_form():
* - form: The form structure to which widgets are being attached. This may be
* a full form structure, or a sub-element of a larger form.
* - field: The field structure.
* - instance: The field instance structure.
* - langcode: The language associated with $items.
* - items: Array of default values for this field.
* - delta: The order of this item in the array of subelements (0, 1, 2, etc).
*
* @see hook_field_widget_form()
* @see hook_field_widget_WIDGET_TYPE_form_alter()
*/
function hook_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
// A hook for changing any widget. Better not use it: it is called on EVERY
// Widget. (Even though the message is only shown once.)
// D7: This hook is introduced in Drupal 7.8.
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', '');
// dpm($context['widget']->getPluginId());
}
function hook_field_widget_workflow_default_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
// A hook specific for the 'workflow_default' widget.
// D7: This hook is introduced in Drupal 7.8.
// D8: This name is specified in the annotation of WorkflowDefaultWidget.
workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', '');
// A widget on an entity form.
if ('workflow_default' == $context['widget']->getPluginId()) {
// This object contains all you need. You may find it in one of two locations.
/* @var $transition WorkflowTransitionInterface */
/* @var $transition WorkflowTransitionInterface */
$transition = $element['#default_value'];
// An example of customizing/overriding the workflow widget.
// Beware, until now, you must do this twice: on the widget and on the form.
if ($transition->getOwnerId() == 1) {
drupal_set_message('(Test/Devel message) I got you, user 1, you will never schedule again,
and you WILL document each state change!', 'warning');
// Let's prohibit scheduling for user 1.
$element['workflow_scheduling']['#access'] = FALSE;
// Let's prohibit scheduling for user 1.
if ($element['comment']['#access'] == TRUE) {
$element['comment']['#required'] = TRUE;
}
}
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*
* Use this hook to alter the form.
* It is only suited if you only use View Page or Workflow Tab.
* If you change the state on the Entity Edit page (form), you need the hook
* hook_form_alter(). See below for more info.
*/
function hook_form_workflow_transition_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, $form_id, '');
// The WorkflowTransitionForm (E.g., Workflow History tab, Block).
// It has its own handling.
// @todo: Populate the WorkflowTransitionForm with a Widget, so we have 1 way-of-working.
// Let's take a (changeable) reference to the element.
$workflow_element = &$form;
// This object contains all you need. You may find it in one of two locations.
/* @var $transition WorkflowTransitionInterface */
$transition = $form['workflow_transition']['#value'];
// dpm($transition);
// An example of customizing/overriding the workflow widget.
// Beware, until now, you must do this twice: on the widget and on the form.
if ($transition->getOwnerId() == 1) {
drupal_set_message('(Test/Devel message) I got you, user 1, you will never schedule again,
and you WILL document each state change!', 'warning');
// Let's prohibit scheduling for user 1.
$workflow_element['workflow_scheduling']['#access'] = FALSE;
// Let's prohibit scheduling for user 1.
if ( $workflow_element['comment']['#access'] == TRUE) {
$workflow_element['comment']['#required'] = TRUE;
}
}
// Get the Entity.
/* @var $entity \Drupal\Core\Entity\EntityInterface */
$entity = NULL;
//$entity = $form['workflow_entity']['#value'];
$entity_type = 'node'; // $form['workflow_entity_type']['#value'];
$entity_bundle = ''; // $entity->bundle();
$sid = '';
if ($entity) {
$entity_type = $entity->getEntityTypeId();
$entity_bundle = $entity->bundle();
// Get the current State ID.
$sid = workflow_node_current_state($entity, $field_name = NULL);
// Get the State object, if needed.
$state = WorkflowState::load($sid);
}
// Change the form, depending on the state ID.
// In the upcoming version 7.x-2.4, States should have a machine_name, too.
if ($entity_type == 'node' && $entity_bundle == 'MY_NODE_TYPE') {
switch ($sid) {
case '2':
// Change form element, form validate and form submit for state '2'.
break;
case '3':
// Change form element, form validate and form submit for state '3'.
break;
}
}
}
/**
* Implements hook_form_alter().
*
* Use this hook to alter the form on an Entity Form, Comment Form (Edit page).
*
* @see hook_form_workflow_transition_form_alter() for example code.
*/
function hook_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if (substr($form_id, 0, 8) == 'workflow') {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, $form_id, '');
}
}
/**
* Hooks defined by core Entity: hook_entity_CRUD.
*
* Instead of using hook_entity_OPERATION, better use hook_ENTITY_TYPE_OPERATION.
*
* @see hook_entity_create(), hook_entity_update(), etc.
* @see hook_ENTITY_TYPE_create(), hook_ENTITY_TYPE_update(), etc.
*/
function hook_entity_predelete(EntityInterface $entity) {
if (substr($entity->getEntityTypeId(), 0, 8) == 'workflow') {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'pre-delete' , $entity->getEntityTypeId());
}
switch ($entity->getEntityTypeId()) {
case 'workflow_config_transition':
case 'workflow_state':
case 'workflow_type':
// Better use hook_ENTITY_TYPE_OPERATION.
// E.g., hook_workflow_type_predelete
break;
}
}
function hook_entity_delete(EntityInterface $entity) {
if (substr($entity->getEntityTypeId(), 0, 8) == 'workflow') {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete' , $entity->getEntityTypeId());
}
}
function hook_workflow_type_delete(EntityInterface $entity) {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete' , $entity->getEntityTypeId());
}
function hook_workflow_config_transition_delete(EntityInterface $entity) {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete' , $entity->getEntityTypeId());
}
function hook_workflow_state_delete(EntityInterface $entity) {
// workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete' , $entity->getEntityTypeId());
}
@@ -0,0 +1,119 @@
<?php
/**
* @file
* Defines a Workflow field, widget and formatter. (copied from list field).
*/
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_FORM_ID_alter().
*
* Changes the hook_field_settings_form.
* Fixes some Field settings and Field Instance settings, and makes sure users cannot change it.
*
* @todo: perhaps this is core functionality, but these values are only saved
* when the site builder explicitly save the instance settings. :-(
*/
function workflow_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$field_name = $form_state->getFormObject()->getEntity()->getType();
if ($field_name == 'workflow') {
// Make sure only 1 value can be entered in the Workflow field.
$form['cardinality_container']['cardinality']['#default_value'] = 'number';
$form['cardinality_container']['cardinality']['#disabled'] = TRUE;
$form['cardinality_container']['cardinality_number']['#default_value'] = 1;
$form['cardinality_container']['cardinality_number']['#disabled'] = TRUE;
$form['cardinality_container']['cardinality_number']['#states'] = [];
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function workflow_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$field_name = $form_state->getFormObject()->getEntity()->getType();
if ($field_name == 'workflow') {
// The Workflow field must have a value, so set to required.
$form['required']['#default_value'] = 1;
$form['required']['#disabled'] = TRUE;
// There are alterations on the widget, too.
// @see WorkflowDefaultWidget::formElement();
}
}
/**
* We will be using some default formatters and widgets from the List and Options modules.
*/
/**
* Implements hook_field_formatter_info_alter().
*
* The module reuses the formatters defined in list.module.
*/
function workflow_field_formatter_info_alter(&$info) {
$info['list_key']['field_types'][] = 'workflow';
$info['list_default']['field_types'][] = 'workflow';
}
/**
* Implements hook_field_widget_info_alter().
*
* The module does not implement widgets of its own, but reuses the
* widgets defined in options.module.
*
* @see workflow_options_list()
*/
function workflow_field_widget_info_alter(&$info) {
$info['options_select']['field_types'][] = 'workflow';
$info['options_buttons']['field_types'][] = 'workflow';
}
/**
* Creates a form element to show the current value of a Workflow state.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity this field is on.
* @param string $field_name
* The field_name
* @param string $current_sid
* The current State Id.
*
* @return array
* Form element, resembling the formatter of List module.
* If state 0 is given, return an empty form element.
*/
function workflow_state_formatter($entity, $field_name, $current_sid = '') {
$element = [];
if (!$current_sid) {
$current_sid = workflow_node_current_state($entity, $field_name);
}
// If user creates a node, and only 1 option is available, the formatter
// is shown with key, not value, because creation state does not count.
// In this case, hide the formatter.
$state = \Drupal\workflow\Entity\WorkflowState::load($current_sid);
if ($state->isCreationState()) {
return $element;
}
// Clone the entity and restore old value, in case you want to show an
// executed transition.
if ($entity->$field_name->value != $current_sid) {
$entity = clone $entity;
$entity->$field_name->value = $current_sid;
}
// Generate a renderable array for the field. Use default language determination ($langcode = NULL).
// First, add the 'current value' formatter for this field.
// $list_display = $instance['display']['default'];
$list_display['type'] = 'list_default';
$element = $entity->$field_name->view($list_display);
// @todo D8: make weight better (even better: hook_field_extra_fields).
// Make sure the current value is before the form. (which has weight = 0.005)
// $element['#weight'] = 0;
return $element;
}
@@ -0,0 +1,277 @@
<?php
/**
* @file
* Contains helper functions for WorkflowTransitionForm.
*/
use Drupal\Component\Utility\Html;
use Drupal\workflow\Entity\WorkflowTransitionInterface;
/**
* Getter/Setter to tell if the action buttons are used.
*
* @param string $button_type
* Options. If empty, value is only getted, else the button type.
*
* @return string
* Previous value. If 'dropbutton'||'buttons', action buttons must be created.
*
* @see workflow_form_alter()
* @see WorkflowDefaultWidget::formElement()
*
* Used to save some expensive operations on every form.
*/
function _workflow_use_action_buttons($button_type = '') {
global $_workflow_use_actions_buttons;
// Reset value if requested.
if ($button_type) {
$_workflow_use_actions_buttons = $button_type;
}
return $_workflow_use_actions_buttons;
}
/**
* Implements hook_form_alter().
*
* Form builder. Move action buttons next to the 'Save'/'Delete' buttons.
*
* This is only used if the set the 'options widget' to 'action buttons'.
* Do not use with multiple workflows per entity: confusing UX.
* ATM this works for:
* - Workflow Field: create, edit, view, workflow tab, comment;
* - Workflow Node: view, workflow tab;
* (For forms with Workflow Node, the form_alter() is AFTER formElement(). )
*
* @todo: move this to WorkflowTransitionForm::_addActionButtons();
*/
function workflow_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
// N.B. Keep code aligned: workflow_form_alter(), WorkflowTransitionForm::actions().
// Use a fast, custom way to check if we need to do this.
// @todo: Make this work with multiple workflows per entity.
if (!_workflow_use_action_buttons()) {
return;
}
if (isset($form_state->getBuildInfo()['base_form_id']) && $form_state->getBuildInfo()['base_form_id'] == 'workflow_transition_form') {
// The WorkflowTransitionForm::actions() has its own handling.
// E.g., Workflow History tab, Block.
return;
}
// Find the first workflow.
// (So this won't work with multiple workflows per entity.)
$workflow_element = _workflow_transition_form_get_first_workflow_element($form);
// Quit if there is no Workflow on this page.
if (!$workflow_element ) {
return;
}
// Quit if there are no Workflow Action buttons.
// (If user has only 1 workflow option, there are no Action buttons.)
if (count($workflow_element['to_sid']['#options']) <= 1) {
return;
}
// Find the default submit button and replace with our own action buttons.
if (isset($form['actions']['submit'])) {
$default_submit_action = $form['actions']['submit'];
// @todo D8-port: on Node-form, this is not sufficient.
unset($form['actions']['submit']);
}
elseif (isset($form['actions']['save'])) {
$default_submit_action = $form['actions']['save'];
unset($form['actions']['save']);
}
else {
// @todo test: when does this happen?
$default_submit_action = [];
}
if (isset($default_submit_action)) {
$actions = _workflow_transition_form_get_action_buttons($form, $workflow_element, $default_submit_action);
// Place the button with the other action buttons.
$form['actions'] = isset($form['actions']) ? $form['actions'] : [];
$form['actions'] += $actions;
}
}
/**
* Fetches the first workflow_element from one of the Workflow Fields.
*
* @param array $form
*
* @return array
*/
function _workflow_transition_form_get_first_workflow_element(&$form) {
$workflow_element = [];
// Find the first workflow.
// (So this won't work with multiple workflows per entity.)
foreach (\Drupal\Core\Render\Element::children($form) as $key) {
if (isset($form[$key]['widget'][0]['#default_value'])) {
$transition = $form[$key]['widget'][0]['#default_value'];
if (is_object($transition) && $transition instanceof WorkflowTransitionInterface ) {
$workflow_element = $form[$key]['widget'][0];
break;
}
}
}
return $workflow_element;
}
/**
* Returns the action buttons from the options widget.
*
* @param array $form
* @param array $workflow_element
* @param array $default_submit_action
*
* @return array $actions
*/
function _workflow_transition_form_get_action_buttons(array $form, array $workflow_element, array $default_submit_action) {
$actions = [];
$current_sid = $workflow_element['to_sid']['#default_value'];
/* @var $transition WorkflowTransitionInterface */
$transition = $workflow_element['workflow_transition']['#value'];
$field_name = $transition->getFieldName();
// Find the default submit button and add our action buttons before it.
// Get the min weight for our buttons.
$option_weight = isset($default_submit_action['#weight']) ? $default_submit_action['#weight'] : 0;
$option_weight = $option_weight - count($workflow_element['to_sid']['#options']);
$min_weight = $option_weight;
foreach ($workflow_element['to_sid']['#options'] as $sid => $option_name) {
// Make the workflow button act exactly like the original submit button.
$same_state_button = ($sid == $current_sid);
$workflow_submit_action = $default_submit_action;
// Add target State ID and Field name, to set correct value in validate_buttons callback.
$workflow_submit_action['#workflow'] = [
'field_name' => $field_name,
'to_sid' => $sid,
];
// Keep option order. Put current state first.
$workflow_submit_action['#weight'] = ($same_state_button) ? $min_weight : ++$option_weight;
// Add/Overwrite some other settings.
$workflow_submit_action['#access'] = TRUE;
$workflow_submit_action['#value'] = $option_name;
// Use one drop button, instead of several action buttons.
if ('dropbutton' == _workflow_use_action_buttons()) {
$workflow_submit_action['#dropbutton'] = 'save';
}
$workflow_submit_action['#attributes'] = ($same_state_button) ? ['class' => ['form-save-default-button']] : [];
$workflow_submit_action['#button_type'] = ($same_state_button) ? 'primary' : ''; // @todo: works for node form and workflow tab, not for workflow block.
//$workflow_submit_action['#executes_submit_callback'] = TRUE;
// Add class to workflow button.
$workflow_submit_action['#attributes']['class'][] = Html::getClass('workflow_button_' . $option_name);
// Append the form's #validate function, or it won't be called upon submit,
// because the workflow buttons have its own #validate.
$workflow_submit_action['#validate'] = [];
$workflow_submit_action['#validate'][] = '_workflow_transition_form_validate_buttons';
if (isset($default_submit_action['#validate'])) {
$workflow_submit_action['#validate'] = $default_submit_action['#validate'];
}
elseif (isset($form['#validate'])) {
$workflow_submit_action['#validate'] = $form['#validate'];
}
// Append the submit-buttons's #submit function, or it won't be called upon submit.
if (isset($default_submit_action['#submit'])) {
$workflow_submit_action['#submit'] = $default_submit_action['#submit'];
}
elseif (isset($form['#submit'])) {
$workflow_submit_action['#submit'] = $form['#submit'];
}
// Hide the same-state button in some cases.
if ($same_state_button) {
if (isset($form['#form_id']) && substr($form['#form_id'], 0, 24) == 'workflow_transition_form') {
// Hide same-state-button on the transition-form (that is:
// view page or workflow history tab) if there is nothing to do.
// However, a Transition may be fieldable.
if ($form['comment']['#access'] == FALSE) {
$workflow_submit_action['#access'] = FALSE;
}
}
elseif (isset($form['#id']) && $form['#id'] == 'comment-form') {
// On comment-form, the button must stay, since you can comment to same state.
}
else {
// On a entity edit page, the button must stay.
}
}
// Place the button with the other action buttons.
$actions['workflow_' . $sid] = $workflow_submit_action;
}
return $actions;
}
/**
* Get the Workflow parameter from the button, pressed by the user.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @return array
* A $field_name => $to_sid array.
*/
function _workflow_transition_form_get_triggering_button(\Drupal\Core\Form\FormStateInterface $form_state) {
$result = ['field_name' => '', 'to_sid' => ''];
$triggering_element = $form_state->getTriggeringElement();
if (isset($triggering_element['#workflow'])) {
$result['field_name'] = $triggering_element['#workflow']['field_name'];
$result['to_sid'] = $triggering_element['#workflow']['to_sid'];
}
return $result;
}
/**
* Submit callback function for the Workflow Form / DefaultWidget.
*
* Validate form data for 'time' element.
* @param $element
* @param \Drupal\Core\Form\FormStateInterface $form_state
* @param $form
*/
function _workflow_transition_form_element_validate_time($element, \Drupal\Core\Form\FormStateInterface &$form_state, $form) {
if (!strtotime($element['#value'])) {
$form_state->setError($element, t('Please enter a valid value for time.'));
}
}
/**
* Submit callback function for the Workflow Form / DefaultWidget.
*
* This is only used when using action buttons in the widget.
* It sets the new state to proper
* element and sets a submit function if needed, making sure the action is
* executed, influencing function core/includes/form.inc/form_execute_handlers().
* (While constructing the Workflow form, we were not yet aware of the submit
* buttons of the complete form. We try to correct this here, without adding
* another hook_form_alter. We guess the first button is the Save button.
*/
function _workflow_transition_form_validate_buttons($form, \Drupal\Core\Form\FormStateInterface &$form_state) {
// Retrieve the data from the form.
$transition = $form_state->getValue('workflow_transition');
if ($transition) {
// On WorkflowTransitionForm :
// D7: $form_state['input']['to_sid'] = $new_sid;
// D7: $form_state['values'][$field_name][$langcode][0]['to_sid'] = $new_sid;
$values = $form_state->getValues();
$to_sid = $form_state->getTriggeringElement()['#workflow']['to_sid'];
$values['to_sid'] = $to_sid;
// Update the form_state.
$form_state->setValues($values);
}
else {
// On edit form : See $form_state->getTriggeringElement() in WorkflowDefaultWidget;
}
}
@@ -0,0 +1,17 @@
name: 'Workflow'
description: 'Defines a field type with Workflows, containing customizable state transitions.'
package: Workflow
type: module
# core: 8.x
# version: VERSION
dependencies:
- field
- options
- user
# Information added by Drupal.org packaging script on 2017-12-17
version: '8.x-1.0'
core: '8.x'
project: 'workflow'
datestamp: 1513552400
@@ -0,0 +1,109 @@
<?php
/**
* @file
* Install, update and uninstall functions for the workflow module.
*/
/**
* Implements hook_install().
*/
function workflow_install() {
$url = \Drupal\Core\Url::fromRoute('system.modules_list', [], ['fragment' => 'edit-modules-workflow']);
$message = t('Thanks for using Workflow. To maintain workflows,
<a href=":url">install the Workflow UI module</a>. To start using
a Workflow, add a Workflow Field to your entity.',
[':url' => $url->toString()]
);
drupal_set_message($message);
}
/**
* Implements hook_uninstall().
*/
function workflow_uninstall() {
// \Drupal::config('workflow.settings')->clear('workflow_states_per_page')->save(); // @FIXME
}
/**
* Implements hook_requirements().
*
* Let admins know that Workflow is in use.
*
* @todo: extend workflow_requirements() for use with Workflow Field API.
*
* @param $phase
* @return array
*/
function workflow_requirements($phase) {
$requirements = [];
switch ($phase) {
case 'install':
break;
case 'update':
break;
case 'runtime':
// workflow_debug(__FILE__), __FUNCTION__, __LINE__, $form_id); // @todo D8-port: still test this snippet.
/*
// Show info on admin/reports/status.
$type_list = 'Not yet determined.';
// $types = db_query('SELECT wid, type FROM {workflow_type_map} WHERE wid <> 0 ORDER BY type')->fetchAllKeyed();
// If there are no types, then just bail.
if (count($types) == 0) {
return;
}
// Let's make it look nice.
if (count($types) == 1) {
$type_list = current($types);
} else {
$last = array_pop($types);
if (count($types) > 2) {
$type_list = implode(', ', $types) . ', and ' . $last;
} else {
$type_list = current($types) . ' and ' . $last;
}
}
*/
/*
$requirements['workflow'] = array(
'title' => $t('Workflow'),
'value' => $t('Workflow is active on the @types content types.', array('@types' => $type_list)),
'severity' => REQUIREMENT_OK,
);
*/
break;
}
return $requirements;
}
/**
* Implements hook_schema().
*
* The D8-schema's have moved to:
* - Workflow annotation (it is a Config Entity now);
* - WorkfowState annotation (it is a Config Entity now);
* - WorkfowConfigTransition annotation (it is a Config Entity now);
* - WorkflowTransition::baseFieldDefinitions();
* - WorkflowScheduledTransition::baseFieldDefinitions().
*/
function workflow_schema() {
return $schema = [];
}
/**
* Drupal 8 updates.
*/
/**
* Update from version beta1 to beta2 are not possible. Please re-install this module.
*/
function workflow_update_8001(&$sandbox) {
drupal_set_message("Update from version beta1 to beta2 is not possible. Please re-install this module.");
}
@@ -0,0 +1,32 @@
#workflow.entities:
# class: \Drupal\Core\Menu\LocalTaskDefault
# deriver: \Drupal\workflow\Plugin\Derivative\WorkflowLocalTask
#entity.workflow_type.canonical:
# route_name: entity.workflow_type.canonical
# base_route: entity.workflow_type.edit_form
# title: 'Edit'
# weight: -5
entity.workflow_type.edit_form:
route_name: entity.workflow_type.canonical
base_route: entity.workflow_type.edit_form
title: 'Edit'
weight: -5
entity.workflow_type.delete_form:
route_name: entity.workflow_type.delete_form
base_route: entity.workflow_type.edit_form
title: 'Delete'
weight: 10
# @todo D8-port: make Workflow History tab happen for every entity_type.
# @see workflow.routing.yml, workflow.links.task.yml, WorkflowTransitionListController.
entity.node.workflow_history:
route_name: entity.node.workflow_history
base_route: entity.node.canonical
title: 'Workflow'
weight: 30
entity.taxonomy_term.edit_form:
route_name: entity.taxonomy_term.workflow_history
base_route: entity.taxonomy_term.canonical
title: 'Workflow'
weight: 30
@@ -0,0 +1,705 @@
<?php
/**
* @file
* Support workflows made up of arbitrary states.
*/
define('WORKFLOW_CREATION_STATE', 1);
define('WORKFLOW_CREATION_DEFAULT_WEIGHT', -50);
define('WORKFLOW_DELETION', 0);
// Couldn't find a more elegant way to preserve translation.
define('WORKFLOW_CREATION_STATE_NAME', 'creation');
/**
* Role ID for anonymous users.
*/
// #2657072 brackets are added later to indicate a special role, and distinguish from frequently used 'author' role.
define('WORKFLOW_ROLE_AUTHOR_NAME', 'Author');
define('WORKFLOW_ROLE_AUTHOR_RID', 'workflow_author');
use Drupal\workflow\Entity\Workflow;
use Drupal\workflow\Entity\WorkflowManager;
use Drupal\workflow\Entity\WorkflowScheduledTransition;
use Drupal\workflow\Entity\WorkflowState;
use Drupal\workflow\Entity\WorkflowTransition;
module_load_include('inc', 'workflow', 'workflow.form');
module_load_include('inc', 'workflow', 'workflow.field');
/**********************************************************************
*
* Info hooks.
*
*/
/**
* Implements hook_help().
*
* @param $route_name
* @return string
*/
function workflow_help($route_name) {
$output = '';
switch ($route_name) {
case 'help.page.workflow':
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Workflow module adds a field to Entities to
store field values as Workflow states. You can control "state transitions"
and add action to specific transitions.') . '</p>';
}
return $output;
}
/**
* Implements hook_hook_info().
*
* Allow adopters to place their hook implementations in either
* their main module or in a module.workflow.inc file.
*/
function workflow_hook_info() {
$hooks['workflow'] = ['group' => 'workflow'];
return $hooks;
}
/**********************************************************************
*
* CRUD hooks.
*
*/
/**
* Implements hook_user_cancel().
*
* Update tables for deleted account, move account to user 0 (anon.)
* ALERT: This may cause previously non-Anonymous posts to suddenly
* be accessible to Anonymous.
*
* @see hook_user_cancel()
*
* @param $edit
* @param $account
* @param $method
*/
function workflow_user_cancel($edit, $account, $method) {
WorkflowManager::cancelUser($edit, $account, $method);
}
/**
* Implements hook_user_delete().
* @todo: hook_user_delete does not exist. hook_ENTITY_TYPE_delete?
*
* @param $account
*/
function workflow_user_delete($account) {
WorkflowManager::deleteUser($account);
}
/**
* Implements hook_ENTITY_TYPE_insert().
*
* Is called when adding a new Workflow type.
* The technical name for the Workflow entity is 'workflow_type'.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*/
function workflow_workflow_type_insert(\Drupal\Core\Entity\EntityInterface $entity) {
WorkflowManager::participateUserRoles($entity);
}
/**
* Implements hook_entity_insert().
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*/
function workflow_entity_insert(\Drupal\Core\Entity\EntityInterface $entity) {
// Execute updates in hook_presave() to revert executions,
// Execute inserts in hook_insert, to have the Entity ID determined.
_workflow_execute_transitions($entity);
}
/**
* Implements hook_entity_presave().
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*/
function workflow_entity_presave(\Drupal\Core\Entity\EntityInterface $entity) {
if (!$entity->isNew()) {
// Avoid a double call by hook_entity_presave and hook_entity_insert.
_workflow_execute_transitions($entity);
}
}
/**
* Execute transitions. if prohibited, restore original field value.
* - insert: use hook_insert(), to have the Entity ID determined when saving transitions.
* - update: use hook_presave() to revert executions,
* - so, do not use hook_update().
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*/
function _workflow_execute_transitions(\Drupal\Core\Entity\EntityInterface $entity) {
// Avoid this hook on workflow objects.
if (WorkflowManager::isWorkflowEntityType($entity->getEntityTypeId())) {
return;
}
// Execute/save the transitions fom the widgets in the entity form.
WorkflowManager::executeTransitionsOfEntity($entity);
}
/**
* Implements hook_entity_delete().
*
* Delete the corresponding workflow table records.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*/
function workflow_entity_delete(\Drupal\Core\Entity\EntityInterface $entity) {
// @todo D8: test with multiple workflows.
if (get_class($entity) == 'Drupal\field\Entity\FieldConfig'
|| get_class($entity) == 'Drupal\field\Entity\FieldStorageConfig') {
// A Workflow Field is removed from an entity.
$field_config = $entity;
/** @var \Drupal\Core\Entity\ContentEntityBase $field_config */
$entity_type = $field_config->get('entity_type');
$field_name = $field_config->get('field_name');
/** @var $transition Drupal\workflow\Entity\WorkflowTransitionInterface */
foreach (WorkflowScheduledTransition::loadMultipleByProperties($entity_type, [], [], $field_name) as $transition) {
$transition->delete();
}
foreach (WorkflowTransition::loadMultipleByProperties($entity_type, [], [], $field_name) as $transition) {
$transition->delete();
}
}
elseif (!WorkflowManager::isWorkflowEntityType($entity->getEntityTypeId())) {
// A 'normal' entity is deleted.
foreach ($fields = _workflow_info_fields($entity) as $field_id => $field_storage) {
$entity_type = $field_storage->getTargetEntityTypeId();
$entity_id = $entity->id();
$field_name = $field_storage->get('field_name');
/** @var $transition Drupal\workflow\Entity\WorkflowTransitionInterface */
foreach (WorkflowScheduledTransition::loadMultipleByProperties($entity_type, [$entity_id], [], $field_name) as $transition) {
$transition->delete();
}
foreach (WorkflowTransition::loadMultipleByProperties($entity_type, [$entity_id], [], $field_name) as $transition) {
$transition->delete();
}
}
}
}
/**
* Implements hook_cron().
*
* Given a time frame, execute all scheduled transitions.
*/
function workflow_cron() {
WorkflowManager::executeScheduledTransitionsBetween(0, \Drupal::time()->getRequestTime());
}
/**
* Business related functions, the API.
*/
/**
* @deprecated D8: @see WorkflowManager::executeTransition().
*
* @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition
* @param bool $force
*
* @return string
*/
function workflow_execute_transition(Drupal\workflow\Entity\WorkflowTransitionInterface $transition, $force = FALSE) {
// Execute transition and update the attached entity.
return $transition->executeAndUpdateEntity($force);
}
/**
* Functions to get an options list (to show in a Widget).
* To be used in non-OO modules, like workflow_rules, workflow_views.
*
* The naming convention is workflow_get_<entity_type>_names.
* (A bit different from 'user_role_names'.)
* Can be used for hook_allowed_values from list.module:
* - user_role
* - workflow
* - workflow_state
* - sid
*/
/**
* Retrieves the names of roles matching specified conditions.
*
* deprecated D8: workflow_get_roles --> workflow_get_user_role_names
*
* Usage:
* D7: $roles = workflow_get_user_role_names('participate in workflow');
* D8: $type_id = $workflow->id();
* D8: $roles = workflow_get_user_role_names("create $type_id workflow_transition");
*
* @param string $permission
* (optional) A string containing a permission. If set, only roles
* containing that permission are returned. Defaults to NULL, which
* returns all roles.
* Normal usage for filtering roles that are enabled in a workflow_type
* would be: $permission = 'create $type_id transition'.
*
* @return array
* Array of role names keyed by role ID, including the 'author' role.
*/
function workflow_get_user_role_names($permission) {
static $roles = NULL;
if ($roles[$permission]) {
return $roles[$permission];
}
// Copied from AccountForm::form().
$roles[$permission] = array_map(['\Drupal\Component\Utility\Html', 'escape'],
[WORKFLOW_ROLE_AUTHOR_RID => '(' . t(WORKFLOW_ROLE_AUTHOR_NAME) . ')']
+ user_role_names(FALSE, $permission));
return $roles[$permission];
}
/**
* Get an options list for workflow states.
*
* @param mixed $wid
* The Workflow ID.
* @param bool $grouped
* Indicates if the value must be grouped per workflow.
* This influences the rendering of the select_list options.
*
* @return array
* An array of $sid => state->label(), grouped per Workflow.
*/
function workflow_get_workflow_state_names($wid = '', $grouped = FALSE) {
$options = [];
// @todo: implement $add parameter.
//
// @todo: follow Options pattern
// @see callback_allowed_values_function()
// @see options_allowed_values()
// Get the (user-dependent) options.
// Since this function is only used in UI, it is save to use the global $user.
$user = workflow_current_user();
/** @var $workflows Workflow[] */
$workflows = Workflow::loadMultiple($wid ? [$wid] : NULL);
// Do not group if only 1 Workflow is configured or selected.
$grouped = count($workflows) == 1 ? FALSE : $grouped;
foreach ($workflows as $wid => $workflow) {
/** @var $state WorkflowState */
$state = WorkflowState::create(['wid' => $wid]);
$workflow_options = $state->getOptions(NULL, '', $user, FALSE);
if (!$grouped) {
$options += $workflow_options;
}
else {
// Make a group for each Workflow.
$options[$workflow->label()] = $workflow_options;
}
}
return $options;
}
/**
* Get an options list for workflows. Include an initial empty value
* if requested. Validate each workflow, and generate a message if not complete.
*
* @param bool $required
* Indicates if the resulting list contains a options value.
* @return array
* An array of $wid => workflow->label().
*/
function workflow_get_workflow_names($required = TRUE) {
$options = [];
if (!$required) {
$options[''] = t('- Select a value -');
}
foreach (Workflow::loadMultiple() as $wid => $workflow) {
/** @var $workflow Workflow */
if ($workflow->isValid()) {
$options[$wid] = $workflow->label();
}
}
return $options;
}
/**
* Gets an Options list of field names.
*
* @param null $entity
* @param string $entity_type
* @param string $entity_bundle
* @param string $field_name
* @return array
*/
function workflow_get_workflow_field_names($entity = NULL, $entity_type = '', $entity_bundle = '', $field_name = '') {
$result = [];
foreach (_workflow_info_fields($entity, $entity_type, $entity_bundle, $field_name) as $definition) {
$field_name = $definition->getName();
$result[$field_name] = $definition->getName();
}
return $result;
}
/**
* Helper function, to get the label of a given State Id.
*
* @param $sid
*
* @return string
*
* deprecated: workflow_get_sid_label() --> workflow_get_sid_name()
*/
function workflow_get_sid_name($sid) {
if (empty($sid)) {
$label = 'No state';
}
/** @noinspection PhpAssignmentInConditionInspection */
elseif ($state = WorkflowState::load($sid)) {
$label = $state->label();
}
else {
$label = 'Unknown state';
}
return t($label);
}
/**
* Determines the Workflow field_name of an entity.
* If an entity has multiple workflows, only returns the first one.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity at hand.
* @param string $field_name
* The field name. If given, will be passed as return value.
*
* @return string
*/
function workflow_get_field_name(\Drupal\Core\Entity\EntityInterface $entity, $field_name = '') {
if (!$entity) {
// $entity may be empty on Entity Add page.
return '';
}
if ($field_name) {
return $field_name;
}
$fields = _workflow_info_fields($entity);
$field = reset($fields);
$field_name = $field->getName();
return $field_name;
}
/**
* Functions to get the state of an entity.
*/
/**
* Wrapper function to get a UserInterface object.
* We use UserInterface to check permissions.
*
* @param \Drupal\Core\Session\AccountInterface|null $account
*
* @return \Drupal\user\UserInterface
*/
function workflow_current_user(\Drupal\Core\Session\AccountInterface $account = NULL) {
$account = ($account) ? $account : \Drupal::currentUser();
return \Drupal\user\Entity\User::load($account->id());
}
/**
* Gets the current state ID of a given entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* @param string $field_name
*
* @return string $current_sid
*
* @deprecated : use WorkflowManager::getCurrentStateId()
*/
function workflow_node_current_state(\Drupal\Core\Entity\EntityInterface $entity, $field_name = '') {
return WorkflowManager::getCurrentStateId($entity, $field_name);
}
/**
* Gets the previous state ID of a given entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* @param string $field_name
*
* @return string $previous_sid
*
* @deprecated : use WorkflowManager::getPreviousStateId()
*/
function workflow_node_previous_state(\Drupal\Core\Entity\EntityInterface $entity, $field_name = '') {
return WorkflowManager::getPreviousStateId($entity, $field_name);
}
/**
* Get a specific workflow, given an entity type. Only one workflow is possible per node type.
*
* @param string $entity_bundle
* An entity bundle.
* @param string $entity_type
* An entity type. This is passed when also the Field API must be checked.
*
* @return Workflow
* A Workflow object, or NULL if no workflow is retrieved.
*
* Caveat: gives undefined results with multiple workflows per entity.
*
* @todo: support multiple workflows per entity.
*/
function workflow_get_workflows_by_type($entity_bundle, $entity_type) {
static $map = [];
if (isset($map[$entity_type][$entity_bundle])) {
return $map[$entity_type][$entity_bundle];
}
$wid = FALSE;
if (isset($entity_type)) {
foreach (_workflow_info_fields(NULL, $entity_type, $entity_bundle) as $field_info) {
$wid = $field_info->getSetting('workflow_type');
}
}
// Set the cache with a workflow object.
$map[$entity_type][$entity_bundle] = NULL;
if ($wid) {
$map[$entity_type][$entity_bundle] = Workflow::load($wid);
}
return $map[$entity_type][$entity_bundle];
}
/**
* Gets the workflow field names, if not known already.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Object to work with. May be empty, e.g., on menu build.
* @param string $entity_type
* Entity type of object. Optional, but required if $entity provided.
* @param string $entity_bundle
* Bundle of entity. Optional.
* @param string $field_name
* Field name. Optional.
*
* @return Drupal\field\Entity\FieldStorageConfig[]
* An array of FieldStorageConfig objects.
*/
function _workflow_info_fields($entity = NULL, $entity_type = '', $entity_bundle = '', $field_name = '') {
$field_info = [];
// Figure out the $entity's bundle and id.
if ($entity) {
$entity_type = $entity->getEntityTypeId();
$entity_bundle = $entity->bundle();
}
else {
// Entity type and bundle should be specified.
}
$field_list = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('workflow');
foreach ($field_list as $e_type => $data) {
if (!$entity_type || ($entity_type == $e_type)) {
foreach ($data as $f_name => $value) {
if (!$entity_bundle || isset($value['bundles'][$entity_bundle])) {
if (!$field_name || ($field_name == $f_name)) {
// Do not use the field_name as ID, but the unique <entity_type>.<field_name>
// since you cannot share the same field on multiple entity_types (unlike D7).
$field_config = \Drupal\field\Entity\FieldStorageConfig::loadByName($e_type, $f_name);
$field_info[$field_config->id()] = $field_config;
}
}
}
}
}
return $field_info;
}
/**
* Helper function to get the entity from a route.
*
* This is a hack. It should be solved by using $route_match.
*
* @param \Drupal\Core\Entity\EntityInterface|null $entity
*
* @return \Drupal\Core\Entity\EntityInterface
*/
function workflow_url_get_entity(\Drupal\Core\Entity\EntityInterface $entity = NULL) {
if ($entity) {
return $entity;
}
$route_match = \Drupal::routeMatch();
// @todo: get entity for any route.
// On node pages, we'd get an object.
$entity = $route_match->getParameter('node');
if ($entity && is_object($entity)) {
return $entity;
}
if ($entity && !is_object($entity)) {
// On workflow tab, we'd get an id.
$entity = \Drupal\node\Entity\Node::load($entity);
return $entity;
}
// It was not a Node, try a Term.
// On term pages, we get objects, or id's.
$entity = $route_match->getParameter('taxonomy_term');
if ($entity && is_object($entity)) {
return $entity;
}
elseif ($entity && !is_object($entity)) {
$entity = \Drupal\taxonomy\Entity\Term::load($entity);
return $entity;
}
if (!$entity) {
// We may be on a entity add page/
// Or we may be on a page of some unknown entity.
}
return $entity;
}
/**
* Helper function to get the field name from a route.
*
* For now only used for ../{entity_id}/workflow history tab.
*
* @return string|null $field_name
*/
function workflow_url_get_field_name() {
/** @var $route_match \Drupal\Core\Routing\RouteMatchInterface */
$route_match = \Drupal::routeMatch();
$field_name = $route_match->getParameter('field_name');
return $field_name;
}
/**
* Helper function to get the entity from a route.
*
* @return mixed|string
*/
function workflow_url_get_operation() {
$url = \Drupal\Core\Url::fromRoute('<current>');
// The last part of the path is the operation: edit, workflow, devel.
$url_parts = explode('/', $url->toString());
$operation = array_pop($url_parts);
// Except for view pages.
if (is_numeric($operation) || $operation == 'view') {
$operation = '';
}
return $operation;
}
/**
* Helper function to determine Workflow from Workflow UI URL.
*
* @param string $url
* @return Workflow
*/
function workflow_url_get_workflow($url = '' ) {
/** @var $workflows \Drupal\workflow\Entity\Workflow[] */
static $workflows = [];
// For some reason, $_SERVER is not allowed as default.
$url = ($url == '') ? $_SERVER['REQUEST_URI'] : $url;
// The URL may have prefixes: /
// ex.1: /en/admin/config/workflow/workflow/MY_WORKFLOW/states
// ex.2: /admin/config/workflow/workflow/MY_WORKFLOW/states
$base_url = '/config/workflow/workflow/';
$string = substr($url, strpos($url, $base_url) + strlen($base_url));
$wid = explode('/', $string)[0];
if (!isset($workflows[$wid])) {
$workflows[$wid] = Workflow::load($wid);
}
return $workflows[$wid];
}
/**
* Helper function to determine the title of the page.
*
* Used in file workflow_ui.routing.yml.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
*/
function workflow_url_get_title() {
$label = '';
// Get the Workflow from the page.
/** @var $workflow \Drupal\workflow\Entity\Workflow */
/** @noinspection PhpAssignmentInConditionInspection */
if ($workflow = workflow_url_get_workflow()) {
$label = $workflow->label();
}
$title = t('Edit @entity %label', ['@entity' => 'Workflow', '%label' => $label]);
return $title;
}
/**
* Helper function to determine Workflow from Workflow UI URL.
*
* @param string $url
* @return mixed
*/
function workflow_url_get_form_type($url = '' ) {
// For some reason, $_SERVER is not allowed as default.
$url = ($url == '') ? $_SERVER['REQUEST_URI'] : $url;
$base_url = '/config/workflow/workflow/';
$string = substr($url, strpos($url, $base_url) + strlen($base_url));
$type = explode('/', $string)[1];
return $type;
}
/**
* Helper function for D8-port: Get some info on screen.
* @see workflow_devel-module
*
* Usage:
* workflow_debug( __FILE__, __FUNCTION__, __LINE__, '', ''); // @todo D8-port: still test this snippet.
*
* @param string $class_name
* @param string $function_name
* @param string $line
* @param string $value1
* @param string $value2
*/
function workflow_debug($class_name, $function_name, $line = '', $value1 = '', $value2 = '') {
$debug_switch = FALSE;
// $debug_switch = TRUE;
if (!$debug_switch) {
return;
}
$class_name_elements = explode( "\\", $class_name);
$output = 'Testing... function ' . end($class_name_elements) . '::' . $function_name . '/' . $line;
if ($value1) {
$output .= ' = ' . $value1;
}
if ($value2) {
$output .= ' > ' . $value2;
}
drupal_set_message($output, 'warning', TRUE);
}
@@ -0,0 +1,10 @@
# Permissions are declared in Workflow (not Workflow UI) so other contrib
# modules can make use of it.
administer workflow:
title: 'Administer workflows'
description: 'Administer all Workflow configurations and settings.'
restrict access: true
permission_callbacks:
- \Drupal\workflow\WorkflowPermissions::workflowTypePermissions
@@ -0,0 +1,144 @@
# All other routes for Workflow maintenance are declared in Workflow UI.
### Workflow CRUD
entity.workflow_type.collection:
path: '/admin/config/workflow/workflow'
defaults:
_entity_list: 'workflow_type'
# _controller: '\workflow\EntityWorkflowUIController::adminOverview'
_title: 'Workflows'
requirements:
_permission: 'administer workflow'
entity.workflow_type.canonical:
path: '/admin/config/workflow/workflow/{workflow_type}'
defaults:
_entity_form: 'workflow_type.edit'
_title: 'Edit Workflow'
_title_callback: 'workflow_url_get_title'
requirements:
_permission: 'administer workflow'
# _entity_access: 'workflow.update'
entity.workflow_type.add_form:
path: '/admin/config/workflow/workflow/add'
defaults:
_entity_form: 'workflow_type.add'
_title: 'Add Workflow'
requirements:
_permission: 'administer workflow'
# _entity_access: 'workflow.update'
entity.workflow_type.edit_form:
path: '/admin/config/workflow/workflow/{workflow_type}'
defaults:
_entity_form: 'workflow_type.edit'
_title: 'Edit Workflow'
_title_callback: 'workflow_url_get_title'
requirements:
_permission: 'administer workflow'
# _entity_access: 'workflow.update'
entity.workflow_type.delete_form:
path: '/admin/config/workflow/workflow/{workflow_type}/delete'
defaults:
_entity_form: 'workflow_type.delete'
_title: 'Delete Workflow'
requirements:
_permission: 'administer workflow'
# _entity_access: 'workflow.update'
### Workflow Executed Transition CRUD
entity.workflow_transition.canonical:
path: '/workflow_transition/{workflow_transition}'
defaults:
# Calls the view controller, defined in the annotation of the contact entity
# _entity_view: 'workflow_transition'
_title: 'Workflow transition'
#requirements:
# Calls the access controller of the entity, $operation 'view'
# _entity_access: 'workflow_transition.view'
entity.workflow_transition.edit_form:
path: '/workflow_transition/{workflow_transition}/edit'
defaults:
_entity_form: 'workflow_transition.edit'
_title: 'Edit Workflow transition'
options:
_admin_route: TRUE
requirements:
_entity_access: 'workflow_transition.update'
entity.workflow_transition.delete_form:
path: '/workflow_transition/{workflow_transition}/delete'
defaults:
_entity_form: 'workflow_transition.delete'
_title: 'Delete Workflow transition'
options:
_admin_route: TRUE
requirements:
_entity_access: 'workflow_transition.delete'
### Workflow Scheduled Transition CRUD
entity.workflow_scheduled_transition.canonical:
path: '/workflow_transition/{workflow_transition}'
defaults:
# Calls the view controller, defined in the annotation of the contact entity
# _entity_view: 'workflow_transition'
_title: 'Workflow transition'
#requirements:
# Calls the access controller of the entity, $operation 'view'
# _entity_access: 'workflow_transition.view'
entity.workflow_scheduled_transition.edit_form:
path: '/workflow_transition/{workflow_transition}/edit'
defaults:
_entity_form: 'workflow_transition.edit'
_title: 'Edit Workflow transition'
options:
_admin_route: TRUE
requirements:
_entity_access: 'workflow_transition.update'
entity.workflow_scheduled_transition.delete_form:
path: '/workflow_transition/{workflow_transition}/delete'
defaults:
_entity_form: 'workflow_transition.delete'
_title: 'Delete Workflow transition'
options:
_admin_route: TRUE
requirements:
_entity_access: 'workflow_transition.delete'
### The Workflow Transition History List (Tab)
# @todo D8-port: make Workflow History tab happen for every entity_type.
# @see workflow.routing.yml, workflow.links.task.yml, WorkflowTransitionListController.
# A route for showing the Workflow history tab.
entity.node.workflow_history:
path: '/node/{node}/workflow/{field_name}'
defaults:
_title: 'Workflow history'
_controller: '\Drupal\workflow\Controller\WorkflowTransitionListController::historyOverview'
field_name: ~
requirements:
_custom_access: '\Drupal\workflow\Controller\WorkflowTransitionListController::historyAccess'
_module_dependencies: 'node'
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type}
entity.taxonomy_term.workflow_history:
path: '/taxonomy/term/{taxonomy_term}/workflow/{field_name}'
defaults:
_title: 'Workflow history'
_controller: '\Drupal\workflow\Controller\WorkflowTransitionListController::historyOverview'
field_name: ~
requirements:
_custom_access: '\Drupal\workflow\Controller\WorkflowTransitionListController::historyAccess'
_module_dependencies: 'taxonomy'
options:
parameters:
entity:
type: entity:{entity_type}
@@ -0,0 +1,10 @@
services:
workflow.manager:
class: Drupal\workflow\Entity\WorkflowManager
arguments: ['@entity_type.manager', '@entity.query', '@config.factory', '@string_translation', '@module_handler', '@current_user']
# workflow.route_subscriber:
# class: Drupal\workflow\Routing\RouteSubscriber
# arguments: ['@entity.manager']
# tags:
# - { name: event_subscriber }
@@ -0,0 +1,78 @@
<?php
/**
* @file
* Provide Views data for workflow.module.
*
* @ingroup views_module_handlers
*/
use Drupal\field\FieldStorageConfigInterface;
/**
* Implements hook_field_views_data().
*/
function workflow_field_views_data(FieldStorageConfigInterface $field) {
$data = views_field_default_views_data($field);
$settings = $field->getSettings();
foreach ($data as $table_name => $table_data) {
foreach ($table_data as $field_name => $field_data) {
if (isset($field_data['filter']) && $field_name != 'delta') {
$data[$table_name][$field_name]['filter']['wid'] = (array_key_exists('workflow_type', $settings)) ? $settings['workflow_type'] : '';
$data[$table_name][$field_name]['filter']['id'] = 'workflow_state';
}
}
}
return $data;
}
/**
* Implements hook_views_data_alter().
*/
function workflow_views_data_alter(array &$data) {
// Provide an integration for each entity type except workflow entities.
// copied from comment.views.inc
foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) {
if (\Drupal\workflow\Entity\WorkflowManager::isWorkflowEntityType($entity_type_id)) {
continue;
}
if (!$entity_type->entityClassImplements(\Drupal\Core\Entity\ContentEntityInterface::class)) {
continue;
}
if (!$entity_type->getBaseTable()) {
continue;
}
$fields = \Drupal::service('workflow.manager')->getFields($entity_type_id);
if ($fields) {
$base_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable();
$args = ['@entity_type' => $entity_type_id];
foreach ($fields as $field_name => $field) {
$data[$base_table][$field_name . '_tid'] = [
'title' => t('Workflow transitions on @entity_type using field: @field_name', $args + ['@field_name' => $field_name]),
'help' => t('Relate all transitions ongit status @entity_type. This will create 1 duplicate record for every transition. Usually if you need this it is better to create a Transition view.', $args),
'relationship' => [
'group' => t('Workflow transition'),
'label' => t('workflow transition'),
'base' => 'workflow_transition_history',
'base field' => 'entity_id',
'relationship field' => $entity_type->getKey('id'),
'id' => 'standard',
'extra' => [
[
'field' => 'entity_type',
'value' => $entity_type_id,
],
[
'field' => 'field_name',
'value' => $field_name,
],
],
],
];
}
}
}
}
@@ -69,6 +69,7 @@ module:
user: 0
views: 0
views_ui: 0
workflow: 0
filefield_sources: 1
menu_admin_per_menu: 1
menu_link_content: 1
@@ -0,0 +1,25 @@
uuid: 4fd977c2-10cc-46c8-b870-a375f4df2ac3
langcode: nl
status: true
dependencies:
module:
- node
- workflow
_core:
default_config_hash: M98hvuYlwD42D6FTasB41hR2kTUpVtEB407qW4Ne1Ww
id: change_a_node_to_next_workflow_state
label: 'Change a node to next Workflow state'
type: node
plugin: workflow_node_next_state_action
configuration:
label: 'Change a node to next Workflow state'
id: change_a_node_to_next_workflow_state
plugin: workflow_node_next_state_action
type: node
workflow_scheduling:
scheduled: '0'
comment: 'New state is set by a triggered Action.'
force: 0
actions: { }
field_name: ''
to_sid: ''
@@ -0,0 +1,716 @@
uuid: 8f1952a2-102b-4873-ac33-1a585844b45a
langcode: fr
status: true
dependencies:
module:
- workflow
_core:
default_config_hash: WVVYWpOBeHeRgm3vCHP0lY65aX-UZx-E_8g6qpFrsHA
id: workflow_entity_history
label: 'Workflow Entity history'
module: views
description: 'Enable this View to configure the history tab.'
tag: ''
base_table: workflow_transition_history
base_field: hid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: table
options:
grouping: { }
row_class: ''
default_row_class: true
override: true
sticky: false
caption: ''
summary: ''
description: ''
columns:
hid: hid
timestamp: timestamp
field_name: field_name
from_sid: from_sid
to_sid: to_sid
uid: uid
comment: comment
operations: operations
info:
hid:
default_sort_order: desc
align: ''
separator: ''
empty_column: false
responsive: ''
timestamp:
align: ''
separator: ''
empty_column: false
responsive: ''
field_name:
align: ''
separator: ''
empty_column: false
responsive: ''
from_sid:
align: ''
separator: ''
empty_column: false
responsive: ''
to_sid:
align: ''
separator: ''
empty_column: false
responsive: ''
uid:
align: ''
separator: ''
empty_column: false
responsive: ''
comment:
align: ''
separator: ''
empty_column: false
responsive: ''
operations:
align: ''
separator: ''
empty_column: false
responsive: ''
default: hid
empty_table: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
hid:
id: hid
table: workflow_transition_history
field: hid
relationship: none
group_type: group
admin_label: ''
label: 'Transition ID'
exclude: true
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: hid
plugin_id: field
timestamp:
id: timestamp
table: workflow_transition_history
field: timestamp
relationship: none
group_type: group
admin_label: ''
label: Timestamp
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: timestamp
settings:
date_format: medium
custom_date_format: ''
timezone: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: timestamp
plugin_id: field
field_name:
id: field_name
table: workflow_transition_history
field: field_name
relationship: none
group_type: group
admin_label: ''
label: 'Field name'
exclude: true
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings:
link_to_entity: false
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: field_name
plugin_id: field
from_sid:
id: from_sid
table: workflow_transition_history
field: from_sid
relationship: none
group_type: group
admin_label: ''
label: 'From state'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: from_sid
plugin_id: field
to_sid:
id: to_sid
table: workflow_transition_history
field: to_sid
relationship: none
group_type: group
admin_label: ''
label: 'To state'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: to_sid
plugin_id: field
uid:
id: uid
table: workflow_transition_history
field: uid
relationship: none
group_type: group
admin_label: ''
label: 'User ID'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: uid
plugin_id: field
comment:
id: comment
table: workflow_transition_history
field: comment
relationship: none
group_type: group
admin_label: ''
label: 'Log message'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: basic_string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: workflow_transition
entity_field: comment
plugin_id: field
operations:
id: operations
table: workflow_transition_history
field: operations
relationship: none
group_type: group
admin_label: ''
label: Operations
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
destination: true
entity_type: null
entity_field: null
plugin_id: entity_operations
filters: { }
sorts: { }
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
entity_id:
id: entity_id
table: workflow_transition_history
field: entity_id
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
default_argument_skip_url: false
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
break_phrase: false
not: false
entity_type: workflow_transition
entity_field: entity_id
plugin_id: numeric
display_extenders: { }
title: 'Workflow history'
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
tags: { }
workflow_history_tab:
display_plugin: embed
id: workflow_history_tab
display_title: 'Workflow history tab'
position: 1
display_options:
display_extenders: { }
display_description: 'Replaces the transition list in the Workflow tab.'
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
tags: { }
@@ -0,0 +1,4 @@
workflow_states_per_page: 20
_core:
default_config_hash: ne-hLr6xAfj-y7himkP4sQpLeAsTLAzt9YuxlQCk8N8
langcode: fr