mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-07 14:25:37 +00:00
Merge branch '3436-chart-widget-frontend' into 'develop'
Resolve "Introduce new chart widget type in the frontend" Closes #3436 See merge request baserow/baserow!3128
This commit is contained in:
commit
5c2511da13
24 changed files with 393 additions and 14 deletions
backend/src/baserow/contrib/dashboard/widgets
enterprise
backend
src/baserow_enterprise
tests/baserow_enterprise_tests
web-frontend/modules/baserow_enterprise
web-frontend/modules
core/assets/scss/components/dashboard
dashboard
|
@ -1,6 +1,7 @@
|
|||
from abc import ABC
|
||||
from decimal import Decimal
|
||||
|
||||
from baserow.contrib.dashboard.models import Dashboard
|
||||
from baserow.contrib.dashboard.types import WidgetDict
|
||||
from baserow.core.registry import (
|
||||
CustomFieldsInstanceMixin,
|
||||
|
@ -32,6 +33,17 @@ class WidgetType(
|
|||
id_mapping_name = DASHBOARD_WIDGETS
|
||||
allowed_fields = ["title", "description"]
|
||||
|
||||
def before_create(self, dashboard: Dashboard):
|
||||
"""
|
||||
This function allows you to perform checks and operations
|
||||
before a widget is created.
|
||||
|
||||
:param dashboard: The dashboard where the widget should be
|
||||
created.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def prepare_value_for_db(self, values: dict, instance: Widget | None = None):
|
||||
"""
|
||||
This function allows you to hook into the moment a widget is created or
|
||||
|
|
|
@ -115,6 +115,8 @@ class WidgetService:
|
|||
|
||||
widget_type_from_registry = widget_type_registry.get(widget_type)
|
||||
|
||||
widget_type_from_registry.before_create(dashboard)
|
||||
|
||||
new_widget = self.handler.create_widget(
|
||||
widget_type_from_registry,
|
||||
dashboard,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from baserow_premium.license.handler import LicenseHandler
|
||||
from rest_framework import serializers
|
||||
|
||||
from baserow.contrib.dashboard.data_sources.handler import DashboardDataSourceHandler
|
||||
|
@ -6,6 +7,7 @@ from baserow.contrib.dashboard.types import WidgetDict
|
|||
from baserow.contrib.dashboard.widgets.models import Widget
|
||||
from baserow.contrib.dashboard.widgets.registries import WidgetType
|
||||
from baserow.core.services.registries import service_type_registry
|
||||
from baserow_enterprise.features import CHART_WIDGET
|
||||
from baserow_enterprise.integrations.local_baserow.service_types import (
|
||||
LocalBaserowGroupedAggregateRowsUserServiceType,
|
||||
)
|
||||
|
@ -31,6 +33,11 @@ class ChartWidgetType(WidgetType):
|
|||
class SerializedDict(WidgetDict):
|
||||
data_source_id: int
|
||||
|
||||
def before_create(self, dashboard):
|
||||
LicenseHandler.raise_if_workspace_doesnt_have_feature(
|
||||
CHART_WIDGET, dashboard.workspace
|
||||
)
|
||||
|
||||
def prepare_value_for_db(self, values: dict, instance: Widget | None = None):
|
||||
if instance is None:
|
||||
# When the widget is being created we want to automatically
|
||||
|
|
|
@ -7,3 +7,4 @@ METRICS = "metrics"
|
|||
SECURE_FILE_SERVE = "secure_file_serve"
|
||||
ENTERPRISE_SETTINGS = "ENTERPRISE_SETTINGS"
|
||||
DATA_SYNC = "data_sync"
|
||||
CHART_WIDGET = "chart_widget"
|
||||
|
|
|
@ -7,6 +7,7 @@ from baserow_premium.license.registries import LicenseType, SeatUsageSummary
|
|||
from baserow.core.models import Workspace
|
||||
from baserow_enterprise.features import (
|
||||
AUDIT_LOG,
|
||||
CHART_WIDGET,
|
||||
DATA_SYNC,
|
||||
ENTERPRISE_SETTINGS,
|
||||
RBAC,
|
||||
|
@ -32,6 +33,7 @@ class EnterpriseWithoutSupportLicenseType(LicenseType):
|
|||
SECURE_FILE_SERVE,
|
||||
ENTERPRISE_SETTINGS,
|
||||
DATA_SYNC,
|
||||
CHART_WIDGET,
|
||||
]
|
||||
instance_wide = True
|
||||
seats_manually_assigned = False
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
|
@ -6,7 +8,9 @@ from baserow.test_utils.helpers import AnyInt
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_create_chart_widget(api_client, enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
user, token = enterprise_data_fixture.create_user_and_token()
|
||||
dashboard = enterprise_data_fixture.create_dashboard_application(user=user)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.deletion import ProtectedError
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -14,7 +15,9 @@ from baserow_enterprise.integrations.local_baserow.models import (
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(DEBUG=True)
|
||||
def test_create_chart_widget_creates_data_source(enterprise_data_fixture):
|
||||
enterprise_data_fixture.enable_enterprise()
|
||||
user = enterprise_data_fixture.create_user()
|
||||
dashboard = enterprise_data_fixture.create_dashboard_application(user=user)
|
||||
widget_type = "chart"
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<svg width="72" height="48" viewBox="0 0 72 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.25" y="1.25" width="69.5" height="45.5" rx="4.75" fill="white"/>
|
||||
<rect x="1.25" y="1.25" width="69.5" height="45.5" rx="4.75" stroke="#E6E6E7" stroke-width="1.5"/>
|
||||
<rect opacity="0.48" x="8" y="15.5" width="6" height="4" rx="2" fill="#E6E6E7"/>
|
||||
<rect opacity="0.48" x="8" y="37" width="6" height="4" rx="2" fill="#E6E6E7"/>
|
||||
<rect opacity="0.48" x="8" y="26.5" width="8" height="4" rx="2" fill="#E6E6E7"/>
|
||||
<rect x="24" y="8" width="1" height="32" fill="#EDEDED"/>
|
||||
<rect x="20" y="39" width="44" height="1" fill="#EDEDED"/>
|
||||
<rect x="20" y="28" width="44" height="1" fill="#EDEDED"/>
|
||||
<rect x="20" y="17" width="44" height="1" fill="#EDEDED"/>
|
||||
<path d="M30 37C30 35.8954 30.8954 35 32 35H34C35.1046 35 36 35.8954 36 37V40H30V37Z" fill="#4E5CFE"/>
|
||||
<path d="M42 25C42 23.8954 42.8954 23 44 23H46C47.1046 23 48 23.8954 48 25V40H42V25Z" fill="#4E5CFE"/>
|
||||
<path d="M54 13C54 11.8954 54.8954 11 56 11H58C59.1046 11 60 11.8954 60 13V40H54V13Z" fill="#4E5CFE"/>
|
||||
</svg>
|
After (image error) Size: 1.1 KiB |
|
@ -18,3 +18,4 @@
|
|||
@import 'common_oidc_setting_form';
|
||||
@import 'saml_auth_link';
|
||||
@import 'oidc_auth_link';
|
||||
@import 'dashboard_chart_widget';
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.dashboard-chart-widget {
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<template><div></div></template>
|
||||
|
||||
<script>
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
|
||||
export default {
|
||||
name: 'GroupedAggregateRowsDataSourceForm',
|
||||
mixins: [form],
|
||||
props: {
|
||||
dashboard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
widget: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
dataSource: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
storePrefix: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="dashboard-chart-widget">
|
||||
<div class="widget-header">
|
||||
<div class="widget-header__main">
|
||||
<div class="widget-header__title-wrapper">
|
||||
<div class="widget-header__title">{{ widget.title }}</div>
|
||||
<div
|
||||
v-if="dataSourceMisconfigured"
|
||||
class="widget-header__fix-configuration"
|
||||
>
|
||||
<svg
|
||||
width="5"
|
||||
height="6"
|
||||
viewBox="0 0 5 6"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="2.5" cy="3" r="2.5" fill="#FF5A44" />
|
||||
</svg>
|
||||
{{ $t('widget.fixConfiguration') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="widget.description" class="widget-header__description">
|
||||
{{ widget.description }}
|
||||
</div>
|
||||
</div>
|
||||
<WidgetContextMenu
|
||||
v-if="isEditMode"
|
||||
:widget="widget"
|
||||
:dashboard="dashboard"
|
||||
@delete-widget="$emit('delete-widget', $event)"
|
||||
></WidgetContextMenu>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WidgetContextMenu from '@baserow/modules/dashboard/components/widget/WidgetContextMenu'
|
||||
|
||||
export default {
|
||||
name: 'ChartWidget',
|
||||
components: { WidgetContextMenu },
|
||||
props: {
|
||||
dashboard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
widget: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
storePrefix: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dataSource() {
|
||||
return this.$store.getters[
|
||||
`${this.storePrefix}dashboardApplication/getDataSourceById`
|
||||
](this.widget.data_source_id)
|
||||
},
|
||||
dataForDataSource() {
|
||||
return this.$store.getters[
|
||||
`${this.storePrefix}dashboardApplication/getDataForDataSource`
|
||||
](this.dataSource?.id)
|
||||
},
|
||||
isEditMode() {
|
||||
return this.$store.getters[
|
||||
`${this.storePrefix}dashboardApplication/isEditMode`
|
||||
]
|
||||
},
|
||||
dataSourceMisconfigured() {
|
||||
const data = this.dataForDataSource
|
||||
if (data) {
|
||||
return !!data._error
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<GroupedAggregateRowsDataSourceForm
|
||||
v-if="dataSource"
|
||||
ref="dataSourceForm"
|
||||
:dashboard="dashboard"
|
||||
:widget="widget"
|
||||
:data-source="dataSource"
|
||||
:default-values="dataSource"
|
||||
:store-prefix="storePrefix"
|
||||
@values-changed="onDataSourceValuesChanged"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GroupedAggregateRowsDataSourceForm from '@baserow_enterprise/dashboard/components/data_source/GroupedAggregateRowsDataSourceForm'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
|
||||
export default {
|
||||
name: 'ChartWidgetSettings',
|
||||
components: { GroupedAggregateRowsDataSourceForm },
|
||||
mixins: [error],
|
||||
props: {
|
||||
dashboard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
widget: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
storePrefix: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dataSource() {
|
||||
return this.$store.getters[
|
||||
`${this.storePrefix}dashboardApplication/getDataSourceById`
|
||||
](this.widget.data_source_id)
|
||||
},
|
||||
integration() {
|
||||
return this.$store.getters[
|
||||
`${this.storePrefix}dashboardApplication/getIntegrationById`
|
||||
](this.dataSource.integration_id)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onDataSourceValuesChanged(changedDataSourceValues) {
|
||||
if (this.$refs.dataSourceForm.isFormValid()) {
|
||||
try {
|
||||
await this.$store.dispatch(
|
||||
`${this.storePrefix}dashboardApplication/updateDataSource`,
|
||||
{
|
||||
dataSourceId: this.dataSource.id,
|
||||
values: changedDataSourceValues,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
this.$refs.dataSourceForm.reset()
|
||||
this.$refs.dataSourceForm.touch()
|
||||
notifyIf(error, 'dashboard')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,44 @@
|
|||
import { WidgetType } from '@baserow/modules/dashboard/widgetTypes'
|
||||
import ChartWidget from '@baserow_enterprise/dashboard/components/widget/ChartWidget'
|
||||
import ChartWidgetSettings from '@baserow_enterprise/dashboard/components/widget/ChartWidgetSettings'
|
||||
import ChartWidgetSvg from '@baserow_enterprise/assets/images/dashboard/widgets/chart_widget.svg'
|
||||
import EnterpriseFeatures from '@baserow_enterprise/features'
|
||||
import EnterpriseModal from '@baserow_enterprise/components/EnterpriseModal'
|
||||
|
||||
export class ChartWidgetType extends WidgetType {
|
||||
static getType() {
|
||||
return 'chart'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('chartWidget.name')
|
||||
}
|
||||
|
||||
get createWidgetImage() {
|
||||
return ChartWidgetSvg
|
||||
}
|
||||
|
||||
get component() {
|
||||
return ChartWidget
|
||||
}
|
||||
|
||||
get settingsComponent() {
|
||||
return ChartWidgetSettings
|
||||
}
|
||||
|
||||
isLoading(widget, data) {
|
||||
const dataSourceId = widget.data_source_id
|
||||
if (data[dataSourceId] && Object.keys(data[dataSourceId]).length !== 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
isAvailable(workspaceId) {
|
||||
return this.app.$hasFeature(EnterpriseFeatures.CHART_WIDGET, workspaceId)
|
||||
}
|
||||
|
||||
getDeactivatedModal() {
|
||||
return EnterpriseModal
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ const EnterpriseFeatures = {
|
|||
AUDIT_LOG: 'AUDIT_LOG',
|
||||
ENTERPRISE_SETTINGS: 'ENTERPRISE_SETTINGS',
|
||||
DATA_SYNC: 'DATA_SYNC',
|
||||
CHART_WIDGET: 'CHART_WIDGET',
|
||||
}
|
||||
|
||||
export default EnterpriseFeatures
|
||||
|
|
|
@ -44,6 +44,7 @@ export class EnterpriseWithoutSupportLicenseType extends LicenseType {
|
|||
EnterpriseFeaturesObject.AUDIT_LOG,
|
||||
EnterpriseFeaturesObject.ENTERPRISE_SETTINGS,
|
||||
EnterpriseFeaturesObject.DATA_SYNC,
|
||||
EnterpriseFeaturesObject.CHART_WIDGET,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -439,5 +439,8 @@
|
|||
},
|
||||
"oidcAuthLink": {
|
||||
"placeholderWithOIDC": "{login} with {provider}"
|
||||
},
|
||||
"chartWidget": {
|
||||
"name": "Chart"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,9 +50,13 @@ import {
|
|||
GitLabIssuesDataSyncType,
|
||||
HubspotContactsDataSyncType,
|
||||
} from '@baserow_enterprise/dataSyncTypes'
|
||||
import { ChartWidgetType } from '@baserow_enterprise/dashboard/widgetTypes'
|
||||
import { PeriodicIntervalFieldsConfigureDataSyncType } from '@baserow_enterprise/configureDataSyncTypes'
|
||||
|
||||
import { FF_AB_SSO } from '@baserow/modules/core/plugins/featureFlags'
|
||||
import {
|
||||
FF_AB_SSO,
|
||||
FF_DASHBOARDS,
|
||||
} from '@baserow/modules/core/plugins/featureFlags'
|
||||
|
||||
export default (context) => {
|
||||
const { app, isDev, store } = context
|
||||
|
@ -155,4 +159,8 @@ export default (context) => {
|
|||
'configureDataSync',
|
||||
new PeriodicIntervalFieldsConfigureDataSyncType(context)
|
||||
)
|
||||
|
||||
if (app.$featureFlagIsEnabled(FF_DASHBOARDS)) {
|
||||
app.$registry.register('dashboardWidget', new ChartWidgetType(context))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,3 +9,4 @@
|
|||
@import 'create_widget_button';
|
||||
@import 'widget_header';
|
||||
@import 'widget_settings_base_form';
|
||||
@import 'create_widget_modal';
|
||||
|
|
|
@ -13,6 +13,11 @@
|
|||
}
|
||||
|
||||
.create-widget-card__name {
|
||||
height: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: $palette-neutral-1200;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
|
@ -20,8 +25,17 @@
|
|||
line-height: 20px;
|
||||
}
|
||||
|
||||
.create-widget-card__name-locked {
|
||||
display: inline-block;
|
||||
color: #4e5cfe;
|
||||
padding: 3px;
|
||||
border-radius: 4px;
|
||||
background: rgba(78, 92, 254, 0.1);
|
||||
}
|
||||
|
||||
.create-widget-card__img-container {
|
||||
padding: 16px 24px;
|
||||
height: 80px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -31,7 +45,7 @@
|
|||
box-shadow: 0 1px 2px 0 rgba(7, 8, 16, 0.1);
|
||||
}
|
||||
|
||||
.create-widget-card:hover .create-widget-card__img-container {
|
||||
.create-widget-card--available:hover .create-widget-card__img-container {
|
||||
border: 1px solid #d7d8d9;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.create-widget-modal__cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 22px;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<a
|
||||
class="create-widget-card"
|
||||
:class="{
|
||||
'create-widget-card--available': isWidgetAvailable,
|
||||
}"
|
||||
@click="widgetTypeSelected"
|
||||
>
|
||||
<div class="create-widget-card__img-container">
|
||||
<img :src="widgetType.createWidgetImage" />
|
||||
</div>
|
||||
<div class="create-widget-card__name">
|
||||
<span>{{ widgetType.name }}</span>
|
||||
<span v-if="!isWidgetAvailable">
|
||||
<i class="iconoir-lock create-widget-card__name-locked"></i>
|
||||
</span>
|
||||
</div>
|
||||
<component
|
||||
:is="deactivatedModal"
|
||||
v-if="deactivatedModal != null"
|
||||
ref="deactivatedModal"
|
||||
:name="widgetType.name"
|
||||
:workspace="dashboard.workspace"
|
||||
></component>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CreateWidgetCard',
|
||||
props: {
|
||||
dashboard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
widgetType: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isWidgetAvailable() {
|
||||
return this.widgetType.isAvailable(this.dashboard.workspace.id)
|
||||
},
|
||||
deactivatedModal() {
|
||||
return this.widgetType.getDeactivatedModal()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
widgetTypeSelected() {
|
||||
if (!this.isWidgetAvailable) {
|
||||
this.$refs.deactivatedModal.show()
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('widget-type-selected', this.widgetType.type)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -3,29 +3,26 @@
|
|||
<h2 class="box__title">
|
||||
{{ $t('createWidgetModal.title') }}
|
||||
</h2>
|
||||
<div>
|
||||
<a
|
||||
<div class="create-widget-modal__cards">
|
||||
<CreateWidgetCard
|
||||
v-for="widgetType in widgetTypes"
|
||||
:key="widgetType.type"
|
||||
class="create-widget-card"
|
||||
@click="widgetTypeSelected(widgetType.type)"
|
||||
:dashboard="dashboard"
|
||||
:widget-type="widgetType"
|
||||
@widget-type-selected="widgetTypeSelected"
|
||||
>
|
||||
<div class="create-widget-card__img-container">
|
||||
<img :src="widgetType.createWidgetImage" />
|
||||
</div>
|
||||
<div class="create-widget-card__name">
|
||||
{{ widgetType.name }}
|
||||
</div>
|
||||
</a>
|
||||
</CreateWidgetCard>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modal from '@baserow/modules/core/mixins/modal'
|
||||
import CreateWidgetCard from '@baserow/modules/dashboard/components/CreateWidgetCard'
|
||||
|
||||
export default {
|
||||
name: 'CreateWidgetModal',
|
||||
components: { CreateWidgetCard },
|
||||
mixins: [modal],
|
||||
props: {
|
||||
dashboard: {
|
||||
|
|
|
@ -36,6 +36,14 @@ export class WidgetType extends Registerable {
|
|||
isLoading(widget, data) {
|
||||
return false
|
||||
}
|
||||
|
||||
isAvailable() {
|
||||
return true
|
||||
}
|
||||
|
||||
getDeactivatedModal() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class SummaryWidgetType extends WidgetType {
|
||||
|
|
Loading…
Add table
Reference in a new issue