form > Settings > EntryStage), drawn as a workshop in five * steps: Access, Columns (and details in EntryStage Pro), Actions, Statuses (EntryStage Pro), Display, Shortcode. * * The page is a custom Gravity Forms form settings page (GFAddOn::form_settings()). It saves with * GFAddOn::save_form_settings(), so the settings live in the form, as with every add-on. * * @package EntryStage */ namespace EntryStage; if ( ! defined( 'ABSPATH' ) ) { exit; } class Atelier { /** * Steps of the page. * * @return array slug => label. */ public static function steps() { return array( 'access' => __( 'Access', 'entrystage' ), 'columns' => __( 'Columns', 'entrystage' ), 'actions' => __( 'Actions', 'entrystage' ), 'display' => __( 'Display', 'entrystage' ), 'shortcode' => __( 'Shortcode', 'entrystage' ), ); } /** * Draws the page (and saves it first when it was posted). * * @param Addon $addon Add-on. * @param array $form Form object. */ public static function render( Addon $addon, $form ) { $message = ''; $error = false; $step = 'access'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked in save(). if ( isset( $_POST['estg_atelier_nonce'] ) ) { list( $message, $error, $step ) = self::save( $addon, $form ); $form = \GFAPI::get_form( (int) $form['id'] ); } Settings::flush(); $settings = Settings::get( $form ); $raw = $addon->get_form_settings( $form ); $raw = is_array( $raw ) ? $raw : array(); $form_id = (int) $form['id']; $d = Settings::defaults(); $list_url = self::list_page_url( $form_id ); $roles = self::roles(); echo '
'; echo '
'; wp_nonce_field( 'estg_atelier_' . $form_id, 'estg_atelier_nonce' ); echo ''; // Header. echo '
'; echo '
'; echo '

' . esc_html__( 'EntryStage · list workshop', 'entrystage' ) . '

' . esc_html( sprintf( /* translators: %s: form title */ __( 'List of "%s"', 'entrystage' ), $form['title'] ) ) . '

'; echo ''; $code = '[entrystage form="' . $form_id . '"]'; echo '
' . esc_html( $code ) . '
'; if ( '' !== $list_url ) { echo '' . esc_html__( 'View the page', 'entrystage' ) . ' '; } echo ''; echo '
'; // Short banner about EntryStage Pro, on this settings page only. The chevron opens the details; // the cross hides it (remembered by the browser). echo ''; if ( '' !== $message ) { echo '
' . esc_html( $message ) . '
'; } if ( ! empty( $settings['columns_migrated'] ) ) { echo '
' . esc_html__( 'The columns come from the field options of an earlier build (form editor). They are already in use; save this page to keep them here. The option has left the form editor.', 'entrystage' ) . '
'; } // Steps. echo ''; // 1. Access. self::panel_open( 'access' ); echo '
'; self::card_open( __( 'Who sees the list', 'entrystage' ) ); self::segmented( 'access', $settings['access'], array( 'everyone' => __( 'Everyone', 'entrystage' ), 'loggedin' => __( 'Logged-in users', 'entrystage' ), 'roles' => __( 'Some roles', 'entrystage' ), ), __( 'Who sees the list', 'entrystage' ) ); echo '

' . esc_html__( 'Roles that see the list', 'entrystage' ) . '

'; foreach ( $roles as $role => $name ) { echo ''; } echo '
'; self::card_close(); self::card_open( __( 'Entries shown', 'entrystage' ) ); self::segmented( 'scope', $settings['scope'], array( 'own' => __( 'Their own entries', 'entrystage' ), 'all' => __( 'All entries', 'entrystage' ), ), __( 'Entries shown', 'entrystage' ) ); echo '

' . esc_html__( 'Each person sees only their own entries, and edits, duplicates and archives them. List managers see and handle every entry.', 'entrystage' ) . '

'; echo '

' . esc_html__( 'Each person sees every entry, but edits, duplicates and archives only their own. List managers handle every entry.', 'entrystage' ) . '

'; self::card_close(); self::card_open( __( 'List managers', 'entrystage' ) ); echo '
' . esc_html( translate_user_role( 'Administrator' ) ) . ''; foreach ( $roles as $role => $name ) { if ( 'administrator' === $role ) { continue; } echo ''; } echo '

' . esc_html__( 'Managers see and handle every entry. Administrators and users who can edit Gravity Forms entries are always managers.', 'entrystage' ) . '

'; self::card_close(); self::card_open( __( 'Page of the form', 'entrystage' ) ); echo ''; echo ''; if ( ! empty( $settings['page_id'] ) ) { /* translators: %s: page title */ echo '

' . esc_html( sprintf( __( 'Form found on the page "%s".', 'entrystage' ), get_the_title( (int) $settings['page_id'] ) ) ) . '

'; } else { echo '

' . esc_html__( 'No page displaying this form was found: Edit and Duplicate stay hidden until one is chosen.', 'entrystage' ) . '

'; } self::card_close(); echo '
'; self::panel_close(); // 2. Columns. self::panel_open( 'columns' ); echo '
'; echo '

' . esc_html__( 'Available fields', 'entrystage' ) . '

'; echo ''; /* translators: %s: number of fields of the form */ echo '

' . esc_html( sprintf( _n( '%s field in this form.', '%s fields in this form.', count( Fields::listable( $form ) ), 'entrystage' ), number_format_i18n( count( Fields::listable( $form ) ) ) ) ) . '

'; echo '
    '; echo '

    ' . esc_html__( 'HTML, Section, Page break, CAPTCHA, Password and Credit card fields are not offered.', 'entrystage' ) . '

    '; // the columns and the details are stacked on the right, each card on two lines. echo '
    '; echo '

    ' . esc_html__( 'Columns of the list', 'entrystage' ) . '

    '; echo '

    ' . esc_html__( 'From left to right in the list. Width: for example 120px or 15%; empty: automatic. The Actions column always comes last; on phones each row becomes a card.', 'entrystage' ) . '

    '; echo '
      '; echo '
      '; echo '
      '; echo '
      '; echo ''; self::panel_close(); // 4. Actions. self::panel_open( 'actions' ); echo '
      '; self::card_open( __( 'Buttons of each row', 'entrystage' ) ); foreach ( array( 'view' => __( 'View', 'entrystage' ), 'edit' => __( 'Edit', 'entrystage' ), 'duplicate' => __( 'Duplicate', 'entrystage' ), 'archive' => __( 'Archive', 'entrystage' ), ) as $action => $label ) { echo '
      '; /* translators: %s: button name */ echo ''; echo '
      '; } self::text( 'actions_header', __( 'Header of the actions column', 'entrystage' ), $settings['actions_header'] ); self::card_close(); self::card_open( __( 'How buttons look', 'entrystage' ) ); self::segmented( 'action_display', $settings['action_display'], array( 'icons' => __( 'Icons', 'entrystage' ), 'both' => __( 'Icons and labels', 'entrystage' ), 'text' => __( 'Labels', 'entrystage' ), ), __( 'How buttons look', 'entrystage' ) ); echo '

      ' . esc_html__( 'Archive means', 'entrystage' ) . '

      '; self::segmented( 'archive_mode', $settings['archive_mode'], array( 'trash' => __( 'Gravity Forms trash', 'entrystage' ), 'delete' => __( 'Permanent deletion', 'entrystage' ), ), __( 'Archive means', 'entrystage' ) ); self::text( 'archive_confirm', __( 'Question before archiving (empty: none)', 'entrystage' ), $settings['archive_confirm'] ); self::select( 'edit_notifications', __( 'Notifications after an edit', 'entrystage' ), $settings['edit_notifications'], array( 'skip' => __( 'Do not send them', 'entrystage' ), 'send' => __( 'Send them, as for a new submission', 'entrystage' ), ) ); self::select( 'after_edit', __( 'After an edit or a copy', 'entrystage' ), $settings['after_edit'], array( 'list' => __( 'Go back to the list', 'entrystage' ), 'confirmation' => __( 'Show the form confirmation', 'entrystage' ), ) ); self::card_close(); echo '
      '; self::panel_close(); // 6. Display. self::panel_open( 'display' ); echo '
      '; self::card_open( __( 'Sorting and pages', 'entrystage' ) ); echo '
      '; echo ''; $sort_choices = array( 'date_created' => __( 'Date of the entry', 'entrystage' ), 'id' => __( 'Entry ID', 'entrystage' ), ); // the columns of the list (and the current choice), not every field of the form. $listable = Fields::listable( $form ); $keys = is_array( $settings['columns'] ) ? wp_list_pluck( $settings['columns'], 'key' ) : array(); $keys[] = (string) $settings['sort_key']; foreach ( array_unique( $keys ) as $id ) { if ( isset( $listable[ $id ] ) && '' !== Fields::sort_key( $listable[ $id ], 'default' ) ) { $sort_choices[ $id ] = Fields::label( $listable[ $id ] ); } } echo '
      '; echo '

      ' . esc_html__( 'Initial sort: date, entry ID or one of the columns of the list (save after changing the columns to see them here).', 'entrystage' ) . '

      '; self::segmented( 'sort_dir', $settings['sort_dir'], array( 'DESC' => __( 'Newest / Z to A first', 'entrystage' ), 'ASC' => __( 'Oldest / A to Z first', 'entrystage' ), ), __( 'Sort direction', 'entrystage' ) ); self::text( 'empty_text', __( 'Text when there is no entry', 'entrystage' ), $settings['empty_text'] ); echo '

      ' . esc_html__( 'Thumbnail size', 'entrystage' ) . '

      '; self::segmented( 'thumb_size', (string) $settings['thumb_size'], array( '48' => __( '48 px', 'entrystage' ), '96' => __( '96 px', 'entrystage' ), ), __( 'Thumbnail size', 'entrystage' ) ); echo '

      ' . esc_html__( 'Thumbnails load the original picture: keep 25 rows per page or fewer when a column shows pictures.', 'entrystage' ) . '

      '; self::card_close(); self::card_open( __( 'Tools above the list', 'entrystage' ) ); self::toggle( 'search', ! empty( $settings['search'] ), __( 'Search box', 'entrystage' ) ); self::toggle( 'ajax', ! empty( $settings['ajax'] ), __( 'Without reloading the page', 'entrystage' ), __( 'Search, sorting, pages and tabs update the list in place. The address of the page follows: it can be shared or reloaded.', 'entrystage' ) ); self::toggle( 'print', ! empty( $settings['print'] ), __( 'Print button', 'entrystage' ), __( 'Opens a printable page with the entries matching the filters, every page (at most 500).', 'entrystage' ) ); self::card_close(); self::card_open( __( 'Files', 'entrystage' ) ); self::toggle( 'secure_files', ! empty( $settings['secure_files'] ), __( 'Downloads only for people who may see the entry', 'entrystage' ), __( 'Off by default. Logged-in users only; the check covers the Gravity Forms download links of this form.', 'entrystage' ) ); echo '
      ' . esc_html__( 'File links in notifications will no longer open for a recipient who is not logged in, and visitors of a public list will not see the thumbnails.', 'entrystage' ) . '
      '; self::card_close(); echo '
      '; self::panel_close(); // 6. Shortcode: a composer and ready-made examples, built from this form. self::panel_open( 'shortcode' ); self::shortcode_panel( $form, $settings ); self::panel_close(); echo ''; echo '
      '; echo ''; echo '
      '; } /** * Saves the posted settings. * * @param Addon $addon Add-on. * @param array $form Form object. * @return array array( message, is error, step to show ). */ private static function save( Addon $addon, $form ) { $form_id = (int) $form['id']; check_admin_referer( 'estg_atelier_' . $form_id, 'estg_atelier_nonce' ); if ( ! \GFCommon::current_user_can_any( 'gravityforms_edit_forms' ) ) { return array( __( 'You are not allowed to change these settings.', 'entrystage' ), true, 'access' ); } $in = isset( $_POST['estg'] ) && is_array( $_POST['estg'] ) ? wp_unslash( $_POST['estg'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- every value is sanitized below. $on = function ( $key ) use ( $in ) { return ! empty( $in[ $key ] ) ? '1' : '0'; }; $text = function ( $key ) use ( $in ) { return isset( $in[ $key ] ) && is_scalar( $in[ $key ] ) ? sanitize_text_field( (string) $in[ $key ] ) : ''; }; $pick = function ( $key, array $allowed, $default ) use ( $in ) { $value = isset( $in[ $key ] ) && is_scalar( $in[ $key ] ) ? (string) $in[ $key ] : ''; return in_array( $value, $allowed, true ) ? $value : $default; }; $d = Settings::defaults(); $roles = array_keys( self::roles() ); $raw = array( 'enabled' => $on( 'enabled' ), 'access' => $pick( 'access', array( 'everyone', 'loggedin', 'roles' ), $d['access'] ), 'scope' => $pick( 'scope', array( 'own', 'all' ), $d['scope'] ), 'page_id' => (string) absint( rgar( $in, 'page_id' ) ), 'per_page' => (string) max( 1, min( 500, absint( rgar( $in, 'per_page' ) ) ? absint( $in['per_page'] ) : $d['per_page'] ) ), 'sort_key' => Settings::clean_key( rgar( $in, 'sort_key' ) ), 'sort_dir' => $pick( 'sort_dir', array( 'ASC', 'DESC' ), $d['sort_dir'] ), 'empty_text' => $text( 'empty_text' ), 'actions_header' => $text( 'actions_header' ), 'action_display' => $pick( 'action_display', array( 'icons', 'both', 'text' ), $d['action_display'] ), 'archive_mode' => $pick( 'archive_mode', array( 'trash', 'delete' ), $d['archive_mode'] ), 'archive_confirm' => $text( 'archive_confirm' ), 'edit_notifications' => $pick( 'edit_notifications', array( 'send', 'skip' ), $d['edit_notifications'] ), 'after_edit' => $pick( 'after_edit', array( 'list', 'confirmation' ), $d['after_edit'] ), 'search' => $on( 'search' ), 'print' => $on( 'print' ), 'ajax' => $on( 'ajax' ), 'thumb_size' => $pick( 'thumb_size', array( '48', '96' ), '48' ), 'secure_files' => $on( 'secure_files' ), 'settings_version' => ENTRYSTAGE_VERSION, ); $actions = array( 'view', 'edit', 'duplicate', 'archive' ); foreach ( $actions as $action ) { $raw[ $action ] = $on( $action ); $raw[ $action . '_label' ] = $text( $action . '_label' ); } foreach ( array( 'access_roles' => 'access_role_', 'manager_roles' => 'manager_role_' ) as $key => $prefix ) { $posted = isset( $in[ $key ] ) && is_array( $in[ $key ] ) ? array_map( 'sanitize_key', $in[ $key ] ) : array(); foreach ( $roles as $role ) { $raw[ $prefix . $role ] = in_array( $role, $posted, true ) ? '1' : '0'; } } $raw['manager_role_administrator'] = '1'; // The column lists are written by the page script; when it did not run, the saved lists are kept. $current = Settings::get( $form ); $posted_columns = trim( (string) rgar( $in, 'columns' ) ); $settings = Settings::from_raw( $raw, $form ); $raw['columns'] = '' !== $posted_columns ? Settings::parse_columns( $posted_columns, $form, $settings ) : (array) $current['columns']; $raw['show_date'] = in_array( '_date', wp_list_pluck( $raw['columns'], 'key' ), true ) ? '1' : '0'; // The Status column belongs to EntryStage Pro: it is kept at its place, for when Pro is activated again. $stored = (array) rgar( (array) $addon->get_form_settings( $form ), 'columns', array() ); foreach ( $stored as $i => $item ) { if ( is_array( $item ) && '_status' === rgar( $item, 'key' ) && ! in_array( '_status', wp_list_pluck( $raw['columns'], 'key' ), true ) ) { array_splice( $raw['columns'], min( (int) $i, count( $raw['columns'] ) ), 0, array( $item ) ); } } // Settings of the other edition (EntryStage Pro or the free EntryStage) that this page does not show are kept. $saved = $addon->get_form_settings( $form ); if ( is_array( $saved ) ) { $raw = array_merge( $saved, $raw ); } /** * Filters the settings of a form before they are saved from the settings page. * * @param array $raw Settings. * @param array $form Form object. */ $raw = (array) apply_filters( 'estg_save_settings', $raw, $form ); $step = $pick( 'step', array_keys( self::steps() ), 'access' ); if ( ! $addon->save_form_settings( $form, $raw ) ) { return array( __( 'The settings could not be saved.', 'entrystage' ), true, $step ); } return array( __( 'Settings saved.', 'entrystage' ), false, $step ); } /** * Data read by the page script. * * @param array $form Form object. * @param array $settings Form settings. * @return array */ private static function data( $form, array $settings ) { $fields = array(); foreach ( Fields::listable( $form ) as $field ) { $fields[] = Fields::describe( $field ); } return array( 'fields' => $fields, 'columns' => array_values( (array) $settings['columns'] ), 'fixed' => array( '_status' => __( 'Status', 'entrystage' ), '_date' => __( 'Date', 'entrystage' ), ), 'thumb' => (int) $settings['thumb_size'], ); } /** * Address of a published page that shows this list, or ''. * * @param int $form_id Form id. * @return string */ private static function list_page_url( $form_id ) { $key = 'estg_list_page_' . (int) $form_id; $found = get_transient( $key ); if ( false === $found ) { global $wpdb; $found = 0; $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- one query, cached below. $wpdb->prepare( "SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status IN ('publish','private') AND post_type NOT IN ('revision','nav_menu_item','attachment') AND post_content LIKE %s ORDER BY ID ASC LIMIT 200", '%' . $wpdb->esc_like( '[entrystage' ) . '%' ) ); foreach ( (array) $rows as $row ) { if ( preg_match( '/\[entrystage\b[^\]]*\b(?:form|id)\s*=\s*["\']?' . (int) $form_id . '\b/i', (string) $row->post_content ) ) { $found = (int) $row->ID; break; } } set_transient( $key, $found, HOUR_IN_SECONDS ); } return $found ? (string) get_permalink( (int) $found ) : ''; } /** * Editable roles. * * @return array role => name. */ private static function roles() { if ( ! function_exists( 'get_editable_roles' ) ) { require_once ABSPATH . 'wp-admin/includes/user.php'; } $out = array(); foreach ( get_editable_roles() as $role => $info ) { $out[ $role ] = translate_user_role( $info['name'] ); } return $out; } /** * Published and private pages. * * @return array id => title. */ private static function pages() { $out = array(); $pages = get_pages( array( 'post_status' => 'publish,private', 'sort_column' => 'post_title', ) ); foreach ( (array) $pages as $page ) { $out[ (int) $page->ID ] = '' !== $page->post_title ? $page->post_title : '#' . $page->ID; } return $out; } /** * Opens a step panel. * * @param string $slug Step. */ private static function panel_open( $slug ) { $steps = self::steps(); echo '
      '; } /** Closes a step panel. */ private static function panel_close() { echo '
      '; } /** * Opens a card. * * @param string $title Title. */ private static function card_open( $title ) { echo '

      ' . esc_html( $title ) . '

      '; } /** Closes a card. */ private static function card_close() { echo '
      '; } /** * Checkbox input. * * @param string $name Setting. * @param bool $checked Checked. * @return string */ private static function checkbox( $name, $checked ) { echo ''; } /** * Switch with a label and an optional help line. * * @param string $name Setting. * @param bool $checked Checked. * @param string $label Label. * @param string $help Help. */ private static function toggle( $name, $checked, $label, $help = '' ) { echo ''; } /** * Shortcode step: a composer (read by the page script) and examples taken from this form. * * @param array $form Form object. * @param array $settings Form settings. */ private static function shortcode_panel( $form, array $settings ) { $form_id = (int) $form['id']; $base = '[entrystage form="' . $form_id . '"]'; echo '
      '; self::card_open( __( 'Build the shortcode', 'entrystage' ) ); echo '

      ' . esc_html__( 'Paste it in a Shortcode block of any page. The options only narrow what this list shows: the rights stay those of the Access step.', 'entrystage' ) . '

      '; echo '
      '; echo ''; echo ''; echo '
      '; /* translators: %d: rows per page set in the Display step */ echo ''; echo '
      '; echo '

      ' . esc_html__( 'Edit and Duplicate open the Gravity Forms form filled in with the entry, so they need a page that shows this form. By default it is the page chosen in the Access step; choose another one only when this list must send people to a different page (for example one page per agency, each showing the form).', 'entrystage' ) . '

      '; echo '
      ' . esc_html( $base ) . '
      '; echo '
      '; self::card_close(); // Examples, with real ids of this form. self::card_open( __( 'Examples', 'entrystage' ) ); $examples = array( array( $base, __( 'The list, with the settings of this form.', 'entrystage' ) ), array( '[entrystage form="' . $form_id . '" user="current"]', __( 'A "My entries" page: each logged-in person sees only their own entries, even when the Access step shows all entries.', 'entrystage' ) ), ); $examples[] = array( '[entrystage form="' . $form_id . '" per_page="50"]', __( '50 rows per page on this page only.', 'entrystage' ) ); $examples[] = array( '[entrystage form="' . $form_id . '" user="5"]', __( 'Only the entries created by the user with ID 5.', 'entrystage' ) ); // Personal space: this form and another form whose list is enabled (or a form number to replace). $other = 0; foreach ( (array) \GFAPI::get_forms( true, false ) as $candidate ) { if ( (int) $candidate['id'] !== $form_id && ! empty( Settings::get( $candidate )['enabled'] ) ) { $other = (int) $candidate['id']; break; } } $examples[] = array( '[entrystage forms="' . $form_id . ',' . ( $other ? $other : 12 ) . '" user="current"]', $other /* translators: 1: id of this form, 2: id of another form */ ? sprintf( __( 'Personal space: one tab per form (here forms %1$d and %2$d), with the entries of the person logged in.', 'entrystage' ), $form_id, $other ) : __( 'Personal space: one tab per form, with the entries of the person logged in. Replace 12 with the ID of another form whose list is enabled.', 'entrystage' ), ); echo ''; echo '

      ' . esc_html__( 'Lists of different forms can be shown on the same page.', 'entrystage' ) . '

      '; self::card_close(); echo '
      '; } /** * Segmented control (radio buttons). * * @param string $name Setting. * @param string $current Current value. * @param array $choices value => label. * @param string $legend Accessible name of the group. */ private static function segmented( $name, $current, array $choices, $legend ) { echo '
      ' . esc_html( $legend ) . ''; foreach ( $choices as $value => $label ) { echo ''; } echo '
      '; } /** * Text box with its label. * * @param string $name Setting. * @param string $label Label. * @param string $value Value. */ private static function text( $name, $label, $value ) { echo ''; } /** * Drop-down with its label. * * @param string $name Setting. * @param string $label Label. * @param string $current Current value. * @param array $choices value => label. */ private static function select( $name, $label, $current, array $choices ) { echo ''; } }