NEW: ModuleBuilder - selectable object tabs (+ fixes for multi-object generation) (#38570)
* NEW : add ModuleBuilder object-tabs map and filter helpers Add getModuleBuilderObjectTabs() (source-of-truth map of optional object tabs) and filterEnabledTabs() (sanitizes user-requested tab keys against the map). Covered by ModuleBuilderLibTest. Next: add BEGIN/END markers around tab flags and blocks in template lib (Task 2) * NEW : wrap object tab flags and blocks with MODULEBUILDER markers Surround each optional tab flag declaration (TABFLAG) and each prepareHead tab block (TAB) with BEGIN/END MODULEBUILDER comment markers, so the generator can purge a whole tab atomically while keeping $h and $head intact. Card tab is not wrapped (always generated). Next: add tab selection checkboxes to the newobject form in modulebuilder/index.php (Task 3) * NEW : add object tab selection checkboxes to newobject form Render one checkbox per optional tab (contact, note, document, agenda) in the ModuleBuilder newobject form, all checked by default and reflecting posted state on redisplay. Next: handle enabledtab in initobject — exclude files, purge blocks, hardcode flags (Task 4) * NEW : apply object tab selection during object generation Excludes page files of unselected tabs, purges their prepareHead flag and block via MODULEBUILDER markers, and hardcodes the show flag to 1 for selected tabs. Emits a best-effort warning when regenerating an existing object. Next: add language keys EnabledTabsForObject(Help) + WarningTabSelectionOnRegeneration (Task 5) * NEW : add language keys for object tab selection Next: add ChangeLog entry (Task 6) * DOC : changelog entry for ModuleBuilder selectable object tabs * FIX : purge agenda card widget when agenda tab is excluded Audit finding: myobject_card.php hardcodes a 'SeeAll' link to myobject_agenda.php in an event widget. When the agenda tab is excluded the page is not generated, leaving a latent dead link if the widget is enabled. Wrap the widget with MODULEBUILDER TAB AGENDA markers and purge it from the generated card when the agenda tab is not selected. Next: functional verification (step 5 of dev workflow) * FIX : check file operation results when applying object tab selection PR review: align with the nogeneratelines pattern by checking the return of removePatternFromFile/dolReplaceInFile and raising $error + dol_syslog with the failing tab/file context, instead of ignoring silent write failures during object generation. Add PR reference to the changelog entry. * FIX : do not abort object generation when nogeneratelines targets a missing file The nogeneratelines handler purges the MODULEBUILDER LINES block from class, API and card files. When API generation is disabled the api_<module>.class.php file does not exist, so removePatternFromFile returns false and $error was incremented, which skipped the whole success path (name substitution, rebuildObjectClass, $tabobj assignment). Generated classes kept the literal 'class MyObject', so dolGetListOfObjectClasses listed every object as MyObject. Guard each purge with file_exists so a missing optional file is skipped. * FIX : keep module descriptor out of the blanket name substitution The 'substitute all module php files' pass applied the object/module name map to every .php file including the module descriptor. That rewrote the persistent /* ... MODULEBUILDER TOPMENU/LEFTMENU MYOBJECT */ marker placeholders to the first object name, so generating a second object could no longer find them (checkExistComment returned -1, 'comments not found for section Menus' warning) and its menu entries were not inserted. Skip the descriptor in that loop: its module name is already resolved by initmodule and its object entries are added by the dedicated menu/permission blocks. * FIX : preserve MODULEBUILDER markers when substituting the module descriptor The previous commit only skipped the descriptor in the secondary substitution pass, but the primary pass (over $filetogenerate, which includes the descriptor) still rewrote its persistent /* ... MODULEBUILDER TOPMENU/LEFTMENU MYOBJECT */ markers to the first object name. Those markers cannot simply be excluded from substitution because the descriptor also has functional MYOBJECT/MYMODULE tokens (e.g. the MYMODULE_MYOBJECT_ADDON numbering constant) that must be resolved. Add dolReplaceInFilePreservingModuleBuilderMarkers(): it hides every MODULEBUILDER marker behind a sentinel, applies the substitution, then restores the markers verbatim. Use it for the descriptor in the primary pass; the descriptor stays excluded from the secondary pass. Generating a second object now finds the markers (checkExistComment) and inserts its menu entries without warning. Verified by CLI: markers preserved across two successive objects, while content (rights, exports, addon constant) is correctly substituted. * FIX : silence phan on the new ModuleBuilder test and typed closure CI phan (diff-only) flagged the freshly added test file: PhanUndeclaredExtendedClass (\CommonClassTest) and PhanUndeclaredMethod (assertSame), like the sibling NamingContractTest. Add the same @phan-file-suppress block (plus PhanTypeMismatchArgumentProbablyReal for the non-array guard test case). Also type the preg_replace_callback parameter (array $matches) to clear PhanPluginUnknownClosureParamType. phpstan is already green; no phpstan annotations added. * FIX : drop closure in marker-preserving substitution to satisfy phan phan's PhanPluginUnknownArrayClosureParamType wanted key/value types on the preg_replace_callback closure parameter. Replace the callback with preg_match_all plus a str_replace loop over the unique markers: same behaviour, no closure, no plugin warning. --------- Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
This commit is contained in:
parent
bae2ca251c
commit
3216098ab5
8 changed files with 258 additions and 2 deletions
|
|
@ -176,6 +176,7 @@ For developers:
|
|||
---------------
|
||||
NEW: Use another hash algorithm v2 based on sha256/hmac for immutable logs (#37725)
|
||||
NEW: Replace MyObject MyModule occurrences (#38370)
|
||||
NEW: ModuleBuilder - Allow selecting which optional tabs (contact, note, document, agenda) are generated for an object (#38570)
|
||||
NEW: add hooks in reception card (#37214)
|
||||
NEW: add new hook in BonPrelevement::EnregDestinataireSEPA() function (#37419)
|
||||
NEW: Add hook selectForFormsListUrl in Form::selectForForms (#37447)
|
||||
|
|
|
|||
|
|
@ -1465,3 +1465,85 @@ function countItemsInDirectory($path, $type = 1)
|
|||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the map of optional tabs that can be generated for a ModuleBuilder object.
|
||||
* The CARD tab is always generated and is therefore not listed here.
|
||||
* HISTORY is an alias of AGENDA (object event history is the agenda tab in Dolibarr).
|
||||
*
|
||||
* @return array<string,array{file:string,var:string,marker:string,label:string}> Map: tab key => metadata
|
||||
*/
|
||||
function getModuleBuilderObjectTabs()
|
||||
{
|
||||
return array(
|
||||
'contact' => array('file' => 'myobject_contact.php', 'var' => 'showtabofpagecontact', 'marker' => 'CONTACT', 'label' => 'Contacts'),
|
||||
'note' => array('file' => 'myobject_note.php', 'var' => 'showtabofpagenote', 'marker' => 'NOTE', 'label' => 'Notes'),
|
||||
'document' => array('file' => 'myobject_document.php', 'var' => 'showtabofpagedocument', 'marker' => 'DOCUMENT', 'label' => 'Documents'),
|
||||
'agenda' => array('file' => 'myobject_agenda.php', 'var' => 'showtabofpageagenda', 'marker' => 'AGENDA', 'label' => 'Events'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a list of requested tab keys against the known optional tabs map.
|
||||
* Protects against injection of unknown keys, removes duplicates, normalizes order.
|
||||
*
|
||||
* @param string[] $requested Raw tab keys requested by the user (e.g. from GETPOST array)
|
||||
* @param array<string,array{file:string,var:string,marker:string,label:string}> $map Map from getModuleBuilderObjectTabs()
|
||||
* @return string[] Sanitized list of valid tab keys, in map order
|
||||
*/
|
||||
function filterEnabledTabs($requested, $map)
|
||||
{
|
||||
$valid = array();
|
||||
if (!is_array($requested) || empty($requested)) {
|
||||
return $valid;
|
||||
}
|
||||
foreach (array_keys($map) as $tabkey) {
|
||||
if (in_array($tabkey, $requested, true)) {
|
||||
$valid[] = $tabkey;
|
||||
}
|
||||
}
|
||||
return $valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply substitutions to a module descriptor file while preserving the MODULEBUILDER comment markers.
|
||||
* Markers such as "BEGIN MODULEBUILDER LEFTMENU MYOBJECT" must keep their MYOBJECT/MYMODULE placeholder
|
||||
* so that generating subsequent objects can still locate them (see checkExistComment()). A blanket
|
||||
* substitution would rewrite them to the first object name and break the generation of further objects.
|
||||
*
|
||||
* @param string $file Path to the module descriptor file
|
||||
* @param array<string,string> $arrayreplacement Substitution map (search => replace), applied as literal strings
|
||||
* @return int 1 on success, -1 on read/write error
|
||||
*/
|
||||
function dolReplaceInFilePreservingModuleBuilderMarkers($file, $arrayreplacement)
|
||||
{
|
||||
if (!file_exists($file)) {
|
||||
return -1;
|
||||
}
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Hide every "/* BEGIN|END MODULEBUILDER ... */" marker behind a sentinel before substituting
|
||||
$foundmarkers = array();
|
||||
preg_match_all('/\/\*\s*(?:BEGIN|END) MODULEBUILDER [^*]*\*\//', $content, $foundmarkers);
|
||||
$sentinels = array();
|
||||
foreach (array_values(array_unique($foundmarkers[0])) as $index => $marker) {
|
||||
$key = "\0MODULEBUILDERMARKER".$index."\0";
|
||||
$sentinels[$key] = $marker;
|
||||
$content = str_replace($marker, $key, $content);
|
||||
}
|
||||
|
||||
$content = str_replace(array_keys($arrayreplacement), array_values($arrayreplacement), $content);
|
||||
|
||||
// Restore the protected markers untouched
|
||||
if (!empty($sentinels)) {
|
||||
$content = strtr($content, $sentinels);
|
||||
}
|
||||
|
||||
if (file_put_contents($file, $content) === false) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,6 +172,9 @@ GeneratePermissions=I want to manage permissions on this object
|
|||
GeneratePermissionsHelp=If you check this, some code will be added to manage permissions to read, write and delete record of the objects
|
||||
NoGenerateLines=I don't want to manage lines on this object
|
||||
NoGenerateLinesHelp=If you check this, some code will be removed to manage lines of the objects
|
||||
EnabledTabsForObject=Tabs to generate for this object
|
||||
EnabledTabsForObjectHelp=Only the selected tabs will generate their page file and tab link. The card tab is always generated. History is the agenda tab.
|
||||
WarningTabSelectionOnRegeneration=This object already exists. Tab selection changes are applied on a best-effort basis on regeneration: already generated files are not overwritten or deleted, and a previously removed tab block cannot be re-injected automatically.
|
||||
PermissionDeletedSuccesfuly=Permission has been successfully removed
|
||||
PermissionUpdatedSuccesfuly=Permission has been successfully updated
|
||||
PermissionAddedSuccesfuly=Permission has been successfully added
|
||||
|
|
|
|||
|
|
@ -170,6 +170,9 @@ DefinePropertiesFromExistingTableDesc=Si une table dans la base de données (pou
|
|||
DefinePropertiesFromExistingTableDesc2=Laisser vide si la table n'existe pas encore. Le générateur de code utilisera différents types de champs pour créer un exemple de table que vous pourrez modifier ultérieurement.
|
||||
GeneratePermissions=Je souhaite gérer les permissions sur cet objet
|
||||
GeneratePermissionsHelp=Si vous cochez ceci, du code sera ajouté pour gérer les permissions de lecture, d'écriture et de suppression des enregistrements des objets.
|
||||
EnabledTabsForObject=Onglets à générer pour cet objet
|
||||
EnabledTabsForObjectHelp=Seuls les onglets sélectionnés généreront leur fichier de page et leur lien d'onglet. L'onglet fiche est toujours généré. L'historique correspond à l'onglet agenda.
|
||||
WarningTabSelectionOnRegeneration=Cet objet existe déjà. Les changements de sélection d'onglets sont appliqués au mieux lors d'une régénération : les fichiers déjà générés ne sont ni écrasés ni supprimés, et un bloc d'onglet précédemment retiré ne peut pas être réinjecté automatiquement.
|
||||
PermissionDeletedSuccesfuly=Les permissions ont été retirées avec succès
|
||||
PermissionUpdatedSuccesfuly=Les permissions ont été mises à jour avec succès
|
||||
PermissionAddedSuccesfuly=Les permissions ont été ajoutées avec succès
|
||||
|
|
|
|||
|
|
@ -1151,6 +1151,10 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
|
|||
$srcdir = DOL_DOCUMENT_ROOT.'/modulebuilder/template';
|
||||
$destdir = $dirins.'/'.strtolower($module);
|
||||
|
||||
// Optional tabs selected by user, and detection of an already generated object (for idempotence warning)
|
||||
$enabledtabs = filterEnabledTabs(GETPOST('enabledtab', 'array'), getModuleBuilderObjectTabs());
|
||||
$objectalreadyexists = dol_is_file($destdir.'/class/'.strtolower($objectname).'.class.php');
|
||||
|
||||
// The dir was not created by init
|
||||
dol_mkdir($destdir.'/class');
|
||||
dol_mkdir($destdir.'/img');
|
||||
|
|
@ -1469,6 +1473,13 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
|
|||
$filetogenerate[$templateFile] = $ncObj->applyToFilename($templateFile);
|
||||
}
|
||||
|
||||
// Exclude tab page files for tabs not selected by user
|
||||
foreach (getModuleBuilderObjectTabs() as $tabkey => $tabinfo) {
|
||||
if (!in_array($tabkey, $enabledtabs, true)) {
|
||||
unset($filetogenerate[$tabinfo['file']]);
|
||||
}
|
||||
}
|
||||
|
||||
if (GETPOST('includerefgeneration', 'aZ09')) {
|
||||
dol_mkdir($destdir.'/core/modules/'.strtolower($module));
|
||||
|
||||
|
|
@ -1727,13 +1738,50 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
|
|||
// Pattern to remove everything between the tags
|
||||
$pattern = '/\/\/BEGIN MODULEBUILDER LINES.*?\/\/END MODULEBUILDER LINES\s*/s';
|
||||
foreach ($TFilePaths as $filePath) {
|
||||
if (! removePatternFromFile($filePath, $pattern)) {
|
||||
// Skip files that were not generated (e.g. the API class when API generation is disabled);
|
||||
// a missing optional file must not abort the whole object generation.
|
||||
if (file_exists($filePath) && !removePatternFromFile($filePath, $pattern)) {
|
||||
$error++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply object tab selection on the generated lib file:
|
||||
// selected tabs -> hardcode the show flag to 1 (visible without extra config) ; unselected -> remove flag declaration and tab block
|
||||
if (!$error) {
|
||||
$libdestfile = $destdir.'/'.$ncObj->applyToFilename('lib/mymodule_myobject.lib.php');
|
||||
foreach (getModuleBuilderObjectTabs() as $tabkey => $tabinfo) {
|
||||
$marker = $tabinfo['marker'];
|
||||
if (in_array($tabkey, $enabledtabs, true)) {
|
||||
$arrayreplacement = array(
|
||||
'/\$'.$tabinfo['var'].' = getDolGlobalInt\([^;]*\);/' => '$'.$tabinfo['var'].' = 1;'
|
||||
);
|
||||
if (dolReplaceInFile($libdestfile, $arrayreplacement, '', '0', 0, 1) < 0) {
|
||||
$error++;
|
||||
dol_syslog("modulebuilder: failed to activate tab flag '".$tabkey."' in ".$libdestfile, LOG_ERR);
|
||||
}
|
||||
} else {
|
||||
if (!removePatternFromFile($libdestfile, '/\h*\/\/ BEGIN MODULEBUILDER TABFLAG '.$marker.'.*?\/\/ END MODULEBUILDER TABFLAG '.$marker.'\s*/s')
|
||||
|| !removePatternFromFile($libdestfile, '/\h*\/\/ BEGIN MODULEBUILDER TAB '.$marker.'.*?\/\/ END MODULEBUILDER TAB '.$marker.'\s*/s')) {
|
||||
$error++;
|
||||
dol_syslog("modulebuilder: failed to purge tab '".$tabkey."' in ".$libdestfile, LOG_ERR);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Agenda has an extra event widget on the card page: purge it too to avoid a dead link when the agenda tab is excluded
|
||||
if (!$error && !in_array('agenda', $enabledtabs, true)) {
|
||||
$carddestfile = $destdir.'/'.$ncObj->applyToFilename('myobject_card.php');
|
||||
if (!removePatternFromFile($carddestfile, '/\h*\/\/ BEGIN MODULEBUILDER TAB AGENDA.*?\/\/ END MODULEBUILDER TAB AGENDA\s*/s')) {
|
||||
$error++;
|
||||
dol_syslog("modulebuilder: failed to purge agenda widget in ".$carddestfile, LOG_ERR);
|
||||
}
|
||||
}
|
||||
if ($objectalreadyexists) {
|
||||
setEventMessages($langs->trans("WarningTabSelectionOnRegeneration"), null, 'warnings');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
// Edit PHP files to make replacement
|
||||
foreach ($filetogenerate as $destfile) {
|
||||
|
|
@ -1752,7 +1800,12 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
|
|||
]
|
||||
);
|
||||
|
||||
$result = dolReplaceInFile($phpfileval['fullname'], $arrayreplacement); // @phpstan-ignore-line
|
||||
if (basename($phpfileval['fullname']) === 'mod'.$module.'.class.php') {
|
||||
// Module descriptor: substitute content but keep the persistent MODULEBUILDER markers intact
|
||||
$result = dolReplaceInFilePreservingModuleBuilderMarkers($phpfileval['fullname'], $arrayreplacement);
|
||||
} else {
|
||||
$result = dolReplaceInFile($phpfileval['fullname'], $arrayreplacement); // @phpstan-ignore-line
|
||||
}
|
||||
//var_dump($result);
|
||||
if ($result < 0) {
|
||||
setEventMessages($langs->trans("ErrorFailToMakeReplacementInto", $phpfileval['fullname']), null, 'errors');
|
||||
|
|
@ -1778,8 +1831,15 @@ if ($dirins && $action == 'initobject' && $module && $objectname) { // Test on
|
|||
]
|
||||
);
|
||||
$allModulePhpFiles = dol_dir_list($destdir, 'files', 1, '\.php$');
|
||||
// The module descriptor must NOT go through the blanket substitution: it keeps persistent
|
||||
// MYOBJECT/MYMODULE markers (TOPMENU/LEFTMENU) reused when generating subsequent objects, and its
|
||||
// own placeholders are already resolved by initmodule and the dedicated menu/permission blocks.
|
||||
$moduledescriptorbasename = 'mod'.$module.'.class.php';
|
||||
if (is_array($allModulePhpFiles) && !empty($allModulePhpFiles)) {
|
||||
foreach ($allModulePhpFiles as $phpFileval) {
|
||||
if (basename($phpFileval['fullname']) === $moduledescriptorbasename) {
|
||||
continue;
|
||||
}
|
||||
$result = dolReplaceInFile($phpFileval['fullname'], $moduleReplacementAll);
|
||||
if ($result < 0) {
|
||||
setEventMessages($langs->trans("ErrorFailToMakeReplacementInto", $phpFileval['fullname']), null, 'warnings');
|
||||
|
|
@ -4234,6 +4294,9 @@ if ($module == 'initmodule') {
|
|||
print '<input type="hidden" name="tab" value="objects">';
|
||||
print '<input type="hidden" name="module" value="'.dol_escape_htmltag($module).'">';
|
||||
|
||||
// Tabs selected by default = all optional tabs; reflect posted state on redisplay
|
||||
$enabledtabsdefault = GETPOSTISSET('enabledtab') ? GETPOST('enabledtab', 'array') : array_keys(getModuleBuilderObjectTabs());
|
||||
|
||||
print '<span class="opacitymedium">'.$langs->trans("EnterNameOfObjectDesc").'</span><br><br>';
|
||||
|
||||
print '<div class="tagtable">';
|
||||
|
|
@ -4273,6 +4336,13 @@ if ($module == 'initmodule') {
|
|||
print '<input type="checkbox" name="includedocgeneration" id="includedocgeneration" value="includedocgeneration"> <label for="includedocgeneration">'.$form->textwithpicto($langs->trans("IncludeDocGeneration"), $langs->trans("IncludeDocGenerationHelp")).'</label><br>';
|
||||
print '<input type="checkbox" name="generatepermissions" id="generatepermissions" value="generatepermissions"> <label for="generatepermissions">'.$form->textwithpicto($langs->trans("GeneratePermissions"), $langs->trans("GeneratePermissionsHelp")).'</label><br>';
|
||||
print '<input type="checkbox" name="nogeneratelines" id="nogeneratelines" value="nogeneratelines"> <label for="nogeneratelines">'.$form->textwithpicto($langs->trans("NoGenerateLines"), $langs->trans("NoGenerateLinesHelp")).'</label><br>';
|
||||
print '<br><span class="opacitymedium">'.$form->textwithpicto($langs->trans("EnabledTabsForObject"), $langs->trans("EnabledTabsForObjectHelp")).'</span><br>';
|
||||
foreach (getModuleBuilderObjectTabs() as $tabkey => $tabinfo) {
|
||||
$checked = in_array($tabkey, $enabledtabsdefault, true) ? ' checked' : '';
|
||||
print '<input type="checkbox" name="enabledtab[]" id="enabledtab_'.$tabkey.'" value="'.dol_escape_htmltag($tabkey).'"'.$checked.'> ';
|
||||
print '<label for="enabledtab_'.$tabkey.'">'.dol_escape_htmltag($langs->trans($tabinfo['label'])).'</label> ';
|
||||
}
|
||||
print '<br>';
|
||||
print '<br>';
|
||||
print '<input type="submit" class="button small" name="create" value="'.dol_escape_htmltag($langs->trans("GenerateCode")).'"'.($dirins ? '' : ' disabled="disabled"').'>';
|
||||
print '<br>';
|
||||
|
|
|
|||
|
|
@ -34,10 +34,18 @@ function myobjectPrepareHead($object)
|
|||
|
||||
$langs->load("mymodule@mymodule");
|
||||
|
||||
// BEGIN MODULEBUILDER TABFLAG CONTACT
|
||||
$showtabofpagecontact = getDolGlobalInt('MAIN_MYMODULE_SHOW_PAGE_OF_CONTACT');
|
||||
// END MODULEBUILDER TABFLAG CONTACT
|
||||
// BEGIN MODULEBUILDER TABFLAG NOTE
|
||||
$showtabofpagenote = getDolGlobalInt('MAIN_MYMODULE_SHOW_PAGE_OF_NOTE');
|
||||
// END MODULEBUILDER TABFLAG NOTE
|
||||
// BEGIN MODULEBUILDER TABFLAG DOCUMENT
|
||||
$showtabofpagedocument = getDolGlobalInt('MAIN_MYMODULE_SHOW_PAGE_OF_DOCUMENT');
|
||||
// END MODULEBUILDER TABFLAG DOCUMENT
|
||||
// BEGIN MODULEBUILDER TABFLAG AGENDA
|
||||
$showtabofpageagenda = getDolGlobalInt('MAIN_MYMODULE_SHOW_PAGE_OF_AGENDA');
|
||||
// END MODULEBUILDER TABFLAG AGENDA
|
||||
|
||||
$h = 0;
|
||||
$head = array();
|
||||
|
|
@ -47,13 +55,16 @@ function myobjectPrepareHead($object)
|
|||
$head[$h][2] = 'card';
|
||||
$h++;
|
||||
|
||||
// BEGIN MODULEBUILDER TAB CONTACT
|
||||
if ($showtabofpagecontact) {
|
||||
$head[$h][0] = dolBuildUrl(dol_buildpath("/mymodule/myobject_contact.php", 1), ['id' => $object->id]);
|
||||
$head[$h][1] = $langs->trans("Contacts");
|
||||
$head[$h][2] = 'contact';
|
||||
$h++;
|
||||
}
|
||||
// END MODULEBUILDER TAB CONTACT
|
||||
|
||||
// BEGIN MODULEBUILDER TAB NOTE
|
||||
if ($showtabofpagenote) {
|
||||
if (isset($object->fields['note_public']) || isset($object->fields['note_private'])) {
|
||||
$nbNote = 0;
|
||||
|
|
@ -72,7 +83,9 @@ function myobjectPrepareHead($object)
|
|||
$h++;
|
||||
}
|
||||
}
|
||||
// END MODULEBUILDER TAB NOTE
|
||||
|
||||
// BEGIN MODULEBUILDER TAB DOCUMENT
|
||||
if ($showtabofpagedocument) {
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/class/link.class.php';
|
||||
|
|
@ -87,13 +100,16 @@ function myobjectPrepareHead($object)
|
|||
$head[$h][2] = 'document';
|
||||
$h++;
|
||||
}
|
||||
// END MODULEBUILDER TAB DOCUMENT
|
||||
|
||||
// BEGIN MODULEBUILDER TAB AGENDA
|
||||
if ($showtabofpageagenda) {
|
||||
$head[$h][0] = dolBuildUrl(dol_buildpath("/mymodule/myobject_agenda.php", 1), ['id' => $object->id]);
|
||||
$head[$h][1] = $langs->trans("Events");
|
||||
$head[$h][2] = 'agenda';
|
||||
$h++;
|
||||
}
|
||||
// END MODULEBUILDER TAB AGENDA
|
||||
|
||||
// Show more tabs from modules
|
||||
// Entries must be declared in modules descriptor with line
|
||||
|
|
|
|||
|
|
@ -637,6 +637,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
|
|||
|
||||
print '</div><div class="fichehalfright">';
|
||||
|
||||
// BEGIN MODULEBUILDER TAB AGENDA
|
||||
$MAXEVENT = 10;
|
||||
|
||||
$morehtmlcenter = dolGetButtonTitle($langs->trans('SeeAll'), '', 'fa fa-bars imgforviewmode', dol_buildpath('/mymodule/myobject_agenda.php', 1).'?id='.$object->id);
|
||||
|
|
@ -649,6 +650,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
|
|||
$formactions = new FormActions($db);
|
||||
$somethingshown = $formactions->showactions($object, $object->element.'@'.$object->module, (is_object($object->thirdparty) ? $object->thirdparty->id : 0), 1, '', $MAXEVENT, '', $morehtmlcenter);
|
||||
}
|
||||
// END MODULEBUILDER TAB AGENDA
|
||||
|
||||
print '</div></div>';
|
||||
}
|
||||
|
|
|
|||
79
test/phpunit/ModuleBuilderLibTest.php
Normal file
79
test/phpunit/ModuleBuilderLibTest.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 ATM Consulting <contact@atm-consulting.fr>
|
||||
*
|
||||
* 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \file test/phpunit/ModuleBuilderLibTest.php
|
||||
* \ingroup test
|
||||
* \brief PHPUnit test for modulebuilder.lib.php tab selection helpers
|
||||
* \remarks To run this script as CLI: phpunit filename.php
|
||||
*/
|
||||
|
||||
global $conf,$user,$langs,$db;
|
||||
require_once dirname(__FILE__).'/../../htdocs/master.inc.php';
|
||||
require_once dirname(__FILE__).'/../../htdocs/core/lib/modulebuilder.lib.php';
|
||||
require_once dirname(__FILE__).'/CommonClassTest.class.php';
|
||||
|
||||
/**
|
||||
* Class for PHPUnit tests
|
||||
*
|
||||
* @backupGlobals disabled
|
||||
* @backupStaticAttributes enabled
|
||||
* @remarks backupGlobals must be disabled to have db,conf,user and lang not erased.
|
||||
* @phan-file-suppress PhanUndeclaredClass
|
||||
* @phan-file-suppress PhanUndeclaredExtendedClass
|
||||
* @phan-file-suppress PhanUndeclaredMethod
|
||||
* @phan-file-suppress PhanTypeMismatchArgumentProbablyReal
|
||||
*/
|
||||
class ModuleBuilderLibTest extends CommonClassTest
|
||||
{
|
||||
/**
|
||||
* testGetModuleBuilderObjectTabs
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testGetModuleBuilderObjectTabs()
|
||||
{
|
||||
$map = getModuleBuilderObjectTabs();
|
||||
$this->assertSame(array('contact', 'note', 'document', 'agenda'), array_keys($map));
|
||||
$this->assertSame('myobject_contact.php', $map['contact']['file']);
|
||||
$this->assertSame('showtabofpageagenda', $map['agenda']['var']);
|
||||
$this->assertSame('DOCUMENT', $map['document']['marker']);
|
||||
}
|
||||
|
||||
/**
|
||||
* testFilterEnabledTabs
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testFilterEnabledTabs()
|
||||
{
|
||||
$map = getModuleBuilderObjectTabs();
|
||||
|
||||
// Nominal: returns requested keys in map order
|
||||
$this->assertSame(array('contact', 'agenda'), filterEnabledTabs(array('agenda', 'contact'), $map));
|
||||
|
||||
// Unknown key is rejected
|
||||
$this->assertSame(array('contact'), filterEnabledTabs(array('contact', 'evil'), $map));
|
||||
|
||||
// Empty / non-array returns empty
|
||||
$this->assertSame(array(), filterEnabledTabs(array(), $map));
|
||||
$this->assertSame(array(), filterEnabledTabs('', $map));
|
||||
|
||||
// Duplicates collapsed
|
||||
$this->assertSame(array('note'), filterEnabledTabs(array('note', 'note'), $map));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue