*/ public function app(): array { return array( 'order' => 30, 'category' => 'tools', 'core' => false, 'deps' => array( ), 'entities' => array( 'library_document' ), 'default' => true, 'label_key' => 'app_document_library', 'desc_key' => 'app_document_library_desc', 'icon' => 'folder', 'cron' => array( ), ); } /** * @return array{label: string, desc: string} */ public function labels(): array { return array( 'label' => __( 'Documents', 'plugixa-crm' ), 'desc' => __( 'A library of files, linked to the records they belong to.', 'plugixa-crm' ), ); } public function migrate(): void { Schema::install(); // The library shares the attachments' private storage directory. The Documents module ensures it // too, but a module that depended on another module's migrate() order would be a module waiting to // break — so it ensures its own, idempotently. Storage::ensure(); // Folders held a hierarchy in `parent_id` alone for several releases, so their closure rows have // to be derived from the tree that is already there. Skipping this would leave every descendant // query answering "nothing" on precisely the installs with folders worth querying. Idempotent. ( new FolderRepository() )->ensure_paths(); } /** * @return array */ public function capabilities(): array { // Covered by the plugin-wide `plugixa_crm_manage` capability, like every other first-class entity // (Leads, Sales). There is no document-specific permission to introduce. return array(); } public function registerServices( Container $container ): void { unset( $container ); } public function registerRoutes( Container $container ): void { $container->get( DocumentLibraryController::class )->register_routes(); } public function registerHooks( Container $container ): void { add_filter( 'plugixa_crm_entity_types', array( $this, 'register_entities' ) ); add_filter( 'plugixa_crm_admin_i18n_strings', static fn( array $strings ): array => array_merge( $strings, Strings::all() ) ); // Whether this user may take a copy from the media library (FileIntake::store_media()), so the // form offers the button only to people the server will let use it. add_filter( 'plugixa_crm_admin_config', static function ( array $config ): array { $config['mediaLibrary'] = current_user_can( 'upload_files' ); return $config; } ); } /** * What must keep listening while this app is switched off — its half of purge, merge, erasure * and uninstall. See LifecycleInterface. * * @param Container $container The container. */ public function registerLifecycle( Container $container ): void { $this->register_cascade( $container ); } /** * Register the Document entity so it gets global search, tags, custom fields and CSV export for free. * * @param array> $entities Registered entities. * * @return array> */ public function register_entities( array $entities ): array { $entities['library_document'] = array( 'label' => __( 'Document', 'plugixa-crm' ), 'plural' => __( 'Documents', 'plugixa-crm' ), // MUST equal the table name — RecordLoader loads by it and TagPermissionsTest enforces it. 'api_base' => 'library_documents', 'fields' => array( 'name' => array( 'label' => __( 'Name', 'plugixa-crm' ), 'type' => 'text', ), 'type' => array( 'label' => __( 'Type', 'plugixa-crm' ), 'type' => 'text', ), 'status' => array( 'label' => __( 'Status', 'plugixa-crm' ), 'type' => 'select', 'options' => array( 'draft' => __( 'Draft', 'plugixa-crm' ), 'active' => __( 'Active', 'plugixa-crm' ), 'cancelled' => __( 'Cancelled', 'plugixa-crm' ), 'expired' => __( 'Expired', 'plugixa-crm' ), ), ), 'publish_date' => array( 'label' => __( 'Publish date', 'plugixa-crm' ), 'type' => 'date', ), 'expiration_date' => array( 'label' => __( 'Expiration date', 'plugixa-crm' ), 'type' => 'date', ), ), ); return $entities; } /** * When a document is purged, its bytes and its links go with it. When a record it was linked to is * purged, the LINKS go — but never the documents, which are first-class and may be linked elsewhere. * * @param Container $container The container. */ private function register_cascade( Container $container ): void { // Merging a duplicate record brings the library documents linked to it. add_filter( 'plugixa_crm_merge_references', static function ( array $references, string $entity_type ): array { if ( in_array( $entity_type, self::LINKABLE, true ) ) { $references[] = array( 'table' => 'document_links', 'id_column' => 'entity_id', 'type_column' => 'entity_type', ); } return $references; }, 10, 2 ); add_action( 'plugixa_crm_before_purge', static function ( string $entity, int $id, array $row ) use ( $container ): void { $links = $container->get( DocumentLinkRepository::class ); // A document destroyed in its own right: unlink its file, drop its associations. if ( self::OWN === $entity ) { Storage::delete( (string) ( $row['disk_name'] ?? '' ) ); $links->purge_for_document( $id ); return; } // A linkable record destroyed: drop the links pointing at it, leave the documents. if ( in_array( $entity, self::LINKABLE, true ) ) { $links->purge_for_record( $entity, $id ); } }, 10, 3 ); } }