Page MenuHomePhabricator

MenuItem url props do not provide option to open links in new tabs
Closed, ResolvedPublic

Description

Description

The url prop for a MenuItem by default opens in the same tab.

Consider providing an option for MenuItem url props to be able to open in new tabs.

Possible workaround:
Use Menu's custom display feature to render the menu item with <a :href="dynamicUrl" target="_blank">
Pros:

  • basic things like highlighted and active styling are preserved
  • works for most simple use cases

Cons:

  • doesn't work with more complexities - i.e. icons

Event Timeline

Catrope triaged this task as Medium priority.Sep 5 2025, 12:33 AM
Catrope moved this task from Inbox to Ready for Design/Dev on the Codex board.
Catrope added a project: Reader Growth Team.
Catrope subscribed.

We could address this by adding a boolean prop named something like urlNewWindow that causes target="_blank" to be set.

Thank you for tagging this task with good first task for Wikimedia newcomers!

Newcomers often may not be aware of things that may seem obvious to seasoned contributors, so please take a moment to reflect on how this task might look to somebody who has never contributed to Wikimedia projects.

A good first task is a self-contained, non-controversial task with a clear approach. It should be well-described with pointers to help a completely new contributor, for example it should clearly point to the codebase URL and provide clear steps to help a contributor get set up for success. We've included some guidelines at https://phabricator.wikimedia.org/tag/good_first_task/ !

Thank you for helping us drive new contributions to our projects <3

Change #1185298 had a related patch set uploaded (by Aditya0545; author: Aditya0545):

[design/codex@main] Codex: Add target prop to support opening links in new tabs

https://gerrit.wikimedia.org/r/1185298

@Adityakumar0545 Hi, please do not ping random people (like me) but be patient. Thanks a lot!

@Adityakumar0545 Hi, please do not ping random people (like me) but be patient. Thanks a lot!

Sorry for the unnecessary ping, I’ll keep this in mind and be patient next time. Thanks for pointing it out.

Change #1185298 merged by jenkins-bot:

[design/codex@main] Codex: Add urlNewTab prop to support opening links in new tabs

https://gerrit.wikimedia.org/r/1185298

Change #1191541 had a related patch set uploaded (by VolkerE; author: VolkerE):

[mediawiki/core@master] Update Codex from v2.3.1 to v2.3.2

https://gerrit.wikimedia.org/r/1191541

Change #1191541 merged by jenkins-bot:

[mediawiki/core@master] Update Codex from v2.3.1 to v2.3.2

https://gerrit.wikimedia.org/r/1191541

hi ! I was trying this fix out and I'm not sure if i'm applying the prop correctly

there are 2 menuItem links that we want to open up in new tabs within the action menu of line items in a table - this is the markup on my local:

Screenshot 2025-10-02 at 3.03.49 PM.png (2,794×790 px, 322 KB)

Screenshot 2025-10-02 at 3.06.47 PM.png (2,678×546 px, 231 KB)

The highlighted lines in the above screenshots show the urlnewtab set to true on the li element - is this where it's supposed to apply? It doesn't appear to work correctly (i.e. opening in same tab) with the following functions and template:

<script setup>
...
function reloadTable() {
    cells = [];
    baseStore.entities.forEach(entity => {
        const entityName = entity.type === 'A/B test' ? 'experiment' : 'instrument';
        const entityCapitalize = entityName.charAt(0).toUpperCase() + entityName.slice(1);
        const url = '/update-' + entityName + '/';
        let actions = [
            { label: 'Edit Configuration', value: 'edit', icon: cdxIconEdit, url: url + entity.slug },
            { label: 'View Phab Ticket', value: 'phab', icon: cdxIconLinkExternal, url: entity.task, urlNewTab: true },
            { label: 'Contact Owner', value: 'contact', icon: cdxIconMessage },
            { label: entity.status ? 'Turn '+ entityCapitalize +' Off' : 'Turn '+ entityCapitalize +' On', value: 'status',
                icon: cdxIconPower, disabled: true },
            { label: 'Delete '+ entityCapitalize +'', value: 'delete', icon: cdxIconTrash },
        ];
        cells.push({
            name: entity.name,
            team: entity.owner.join(", "),
            type: entity.type,
            start_date: new Date(entity.utc_start_dt),
            progress: Math.round(entity.progress * 100) + ' %',
            status: entity.status ? 'On' : 'Off',
            actions: actions,
            slug: entity.slug,
            instrument: entity
        })
    });

    data.value = cells;
}

...

function setMenuItems(row) {
    // Add first object to action menu
    let actions = [{ label: 'Edit', value: row.instrument.type === 'A/B test' ? 'update-experiment' : 'update-instrument', icon: cdxIconEdit }];

    let canBeActivatedStartTime = true;
    let canBeActivatedRiskLevel = true;
    let entity = row.experiment === undefined ? 'instrument' : 'experiment';

    const activationMessage = 'Turn ' + (row[entity].status ? 'Off' : 'On');
    let activationDescription;

    // Update actions menu with activation message and results if the item row is an experiment
    if (row[entity].type === 'A/B test') {
        // Calculates whether the experiment can be activated and the corresponding message
        if (row[entity].status === 0) {
            let todayPlus24Hours = new UTCDate();
            todayPlus24Hours.setDate(todayPlus24Hours.getDate() + 1);

            const entityStartDate = new Date(row[entity].utc_start_dt);
            canBeActivatedStartTime = entityStartDate > todayPlus24Hours;
            if (!canBeActivatedStartTime) {
                activationDescription = 'Start date must be 24h in the future. Currently set to ' + row[entity].utc_start_dt;
            }
        }

        // Determines whether the experiment's results can be accessed and the corresponding message
        let analytics_uri = registerExperimentLink;
        if (experimentStore.registeredExperiments.includes(row[entity].slug)) {
            analytics_uri = getAnalyticsUri( row[entity].name, row[entity].sample_unit );
        }
        const experimentResultsLabel = analytics_uri === registerExperimentLink ?
            'View results - This experiment is not registered for automated analysis. Learn more' :
            'View results in Superset';
        const experimentResults = {
            label: experimentResultsLabel,
            value: 'analytics',
            icon: cdxIconChart,
            url: analytics_uri,
            urlNewTab: true
        };
        actions.push(experimentResults);
    }

    // Calculates whether an experiment/instrument can be activated based on its risk level
    if (row[entity].status === 0) {
        canBeActivatedRiskLevel = row[entity].risk_level !== 'Risk assessment pending';
        if (!canBeActivatedRiskLevel) {
            activationDescription = 'Risk level must be set with along the security legal review when needed'
        }
    }

    const canBeActivated = canBeActivatedStartTime && canBeActivatedRiskLevel;

    // Append the rest of the action menu items
    actions.push(
        { label: 'View Phabricator task', value: 'phab', icon: cdxIconLinkExternal, urlNewTab: true },
        { label: 'Contact Owner', value: 'contact', icon: cdxIconMessage },
        { label: activationMessage, description: activationDescription, value: 'status', icon: cdxIconPower, disabled: !canBeActivated },
        { label: 'Delete', value: 'delete', icon: cdxIconTrash, action: 'destructive'}
    );

    return actions;
}

...

function handleAction(row) {
    let entity = row.experiment === undefined ? 'instrument' : 'experiment';
    baseStore.setEntity(row[entity]);

    switch (selection.value) {
        case 'update-experiment':
            router.push(row.actions[0].url);
        case 'update-instrument':
            router.push(row.actions[0].url);
            break;
        case 'phab':
            window.location.href = row.actions[1].url;
            break;
        case 'status':
            activationWarning.value = true
            break;
        case 'delete':
            deleteWarning.value = true;
            break;
    }

    selection.value = null;
}
...

</script>
<template>
...
        <cdx-table ref="table"
            v-model:sort="sort"
            class="cdx-docs-table-with-sort"
            caption="Experiments and instruments"
            :columns="columns"
            :data="data"
            @update:sort="onSort"
            :paginate="true"
            :paginationSizeOptions="[ { value: 10 }, { value: 20 }, { value: 30 } ]"
            :pagination-size-default="10"
        >
            <template #header>
                <cdx-search-input
                    placeholder="Search"
                    @update:model-value="onSearch($event)"
                    autofocus
                />
            </template>

            <template #item-name="{ row, item }">
                <router-link class="cdx-docs-link" :to="`/${ row.type === 'instrument' ? 'instrument' : 'experiment' }/${ row.slug }`">
                    {{ item }}
                </router-link>
            </template>

            <template #item-start_date="{ item }">
                {{ getFormattedDate( item ) }}
            </template>

            <!-- Get data for the entire row to grab the block id and perform an action. -->
            <template #item-actions="{ row }">
                <div class="cdx-docs-table-custom-cells__actions">
                    <cdx-menu-button
                        v-model:selected="selection"
                        :menu-items="setMenuItems(row)"
                        @update:selected="handleAction(row)"
                        aria-label="Change input type"
                    >
                        <cdx-icon :icon="cdxIconEllipsis" />
                    </cdx-menu-button>
                </div>
            </template>

            <template #empty-state>
                {{ emptyMessage }}
            </template>
        </cdx-table>
...
</template>

setMenuItems(row) + handleAction(row) are the functions that build the menu items per row in the cdx-menu-button component

if it's helpful to see the full file, I can push a WIP MR for the full context

nevermind above -- things are working as expected (updated the wrong package.json)