Skip to content

Commit d20a3bd

Browse files
committedApr 1, 2020
[FEATURE][processing] Port output parameter wrappers to new c++ API for modeler
This allows a range of new possibilities, including: - models with static outputs for child algorithms, e.g. always saving a child algorithm's output to a geopackage or postgres layer - models with expression based output values for child algorithms, e.g. generating an automatic file name based on today's date and saving outputs to that file
1 parent 3d9bd71 commit d20a3bd

11 files changed

+620
-54
lines changed
 

‎python/core/auto_generated/processing/models/qgsprocessingmodelchildparametersource.sip.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Source for the value of a parameter for a child algorithm within a model.
3030
StaticValue,
3131
Expression,
3232
ExpressionText,
33+
ModelOutput,
3334
};
3435

3536
QgsProcessingModelChildParameterSource();

‎python/gui/auto_generated/processing/qgsprocessingmodelerparameterwidget.sip.in

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ from QgsProcessing.SourceType.
9191
void setExpressionHelpText( const QString &text );
9292
%Docstring
9393
Set the expected expression format ``text``, which is shown in the expression builder dialog for the widget
94-
when in the "pre-calculated" expression mode. This is purely a text format and no expression validation is made
94+
when in the "pre-calculated" ex pression mode. This is purely a text format and no expression validation is made
9595
against it.
9696
%End
9797

@@ -120,6 +120,39 @@ Sets the current ``values`` for the parameter.
120120

121121
.. seealso:: :py:func:`value`
122122

123+
.. versionadded:: 3.14
124+
%End
125+
126+
void setToModelOutput( const QString &value );
127+
%Docstring
128+
Sets the widget to a model output, for destination parameters only.
129+
130+
.. seealso:: :py:func:`isModelOutput`
131+
132+
.. seealso:: :py:func:`modelOutputName`
133+
134+
.. versionadded:: 3.14
135+
%End
136+
137+
bool isModelOutput() const;
138+
%Docstring
139+
Returns ``True`` if the widget is set to the model output mode.
140+
141+
.. seealso:: :py:func:`setToModelOutput`
142+
143+
.. seealso:: :py:func:`modelOutputName`
144+
145+
.. versionadded:: 3.14
146+
%End
147+
148+
QString modelOutputName() const;
149+
%Docstring
150+
Returns the model output name, if isModelOutput() is ``True``.
151+
152+
.. seealso:: :py:func:`setToModelOutput`
153+
154+
.. seealso:: :py:func:`isModelOutput`
155+
123156
.. versionadded:: 3.14
124157
%End
125158

‎python/plugins/processing/modeler/ModelerParametersDialog.py

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,6 @@
3535
QgsProcessingModelOutput,
3636
QgsProcessingModelChildAlgorithm,
3737
QgsProcessingModelChildParameterSource,
38-
QgsProcessingParameterFeatureSink,
39-
QgsProcessingParameterRasterDestination,
40-
QgsProcessingParameterFileDestination,
41-
QgsProcessingParameterFolderDestination,
42-
QgsProcessingParameterVectorDestination,
4338
QgsProcessingOutputDefinition)
4439

4540
from qgis.gui import (QgsGui,
@@ -175,11 +170,8 @@ def algorithm(self):
175170
return self._alg
176171

177172
def setupUi(self):
178-
self.checkBoxes = {}
179173
self.showAdvanced = False
180174
self.wrappers = {}
181-
self.valueItems = {}
182-
self.dependentItems = {}
183175
self.algorithmItem = None
184176

185177
self.mainLayout = QVBoxLayout()
@@ -246,8 +238,6 @@ def setupUi(self):
246238
else:
247239
widget = wrapper.widget
248240
if widget is not None:
249-
self.valueItems[param.name()] = widget
250-
251241
if issubclass(wrapper.__class__, QgsProcessingModelerParameterWidget):
252242
label = wrapper.createLabel()
253243
else:
@@ -263,19 +253,29 @@ def setupUi(self):
263253
self.verticalLayout.addWidget(label)
264254
self.verticalLayout.addWidget(widget)
265255

266-
for dest in self._alg.destinationParameterDefinitions():
267-
if dest.flags() & QgsProcessingParameterDefinition.FlagHidden:
256+
for output in self._alg.destinationParameterDefinitions():
257+
if output.flags() & QgsProcessingParameterDefinition.FlagHidden:
268258
continue
269-
if isinstance(dest, (QgsProcessingParameterRasterDestination, QgsProcessingParameterVectorDestination,
270-
QgsProcessingParameterFeatureSink, QgsProcessingParameterFileDestination,
271-
QgsProcessingParameterFolderDestination)):
272-
label = QLabel(dest.description())
273-
item = QgsFilterLineEdit()
274-
if hasattr(item, 'setPlaceholderText'):
275-
item.setPlaceholderText(self.tr('[Enter name if this is a final result]'))
259+
260+
widget = QgsGui.processingGuiRegistry().createModelerParameterWidget(self.model,
261+
self.childId,
262+
output,
263+
self.context)
264+
widget.setDialog(self.dialog)
265+
widget.setWidgetContext(widget_context)
266+
widget.registerProcessingContextGenerator(self.context_generator)
267+
268+
self.wrappers[output.name()] = widget
269+
270+
item = QgsFilterLineEdit()
271+
if hasattr(item, 'setPlaceholderText'):
272+
item.setPlaceholderText(self.tr('[Enter name if this is a final result]'))
273+
274+
label = widget.createLabel()
275+
if label is not None:
276276
self.verticalLayout.addWidget(label)
277-
self.verticalLayout.addWidget(item)
278-
self.valueItems[dest.name()] = item
277+
278+
self.verticalLayout.addWidget(widget)
279279

280280
label = QLabel(' ')
281281
self.verticalLayout.addWidget(label)
@@ -401,9 +401,34 @@ def setPreviousValues(self):
401401
value = value.staticValue()
402402
wrapper.setValue(value)
403403

404-
for name, out in alg.modelOutputs().items():
405-
if out.childOutputName() in self.valueItems:
406-
self.valueItems[out.childOutputName()].setText(out.name())
404+
for output in self.algorithm().destinationParameterDefinitions():
405+
if output.flags() & QgsProcessingParameterDefinition.FlagHidden:
406+
continue
407+
408+
model_output_name = None
409+
for name, out in alg.modelOutputs().items():
410+
if out.childId() == self.childId and out.childOutputName() == output.name():
411+
# this destination parameter is linked to a model output
412+
model_output_name = out.name()
413+
break
414+
415+
value = None
416+
if model_output_name is None and output.name() in alg.parameterSources():
417+
value = alg.parameterSources()[output.name()]
418+
if isinstance(value, list) and len(value) == 1:
419+
value = value[0]
420+
elif isinstance(value, list) and len(value) == 0:
421+
value = None
422+
423+
wrapper = self.wrappers[output.name()]
424+
425+
if model_output_name is not None:
426+
wrapper.setToModelOutput(model_output_name)
427+
elif value is not None or output.defaultValue() is not None:
428+
if value is None:
429+
value = QgsProcessingModelChildParameterSource.fromStaticValue(output.defaultValue())
430+
431+
wrapper.setWidgetValue(value)
407432

408433
selected = []
409434
dependencies = self.getAvailableDependencies()
@@ -457,21 +482,31 @@ def createAlgorithm(self):
457482
alg.addParameterSources(param.name(), val)
458483

459484
outputs = {}
460-
for dest in self._alg.destinationParameterDefinitions():
461-
if not dest.flags() & QgsProcessingParameterDefinition.FlagHidden:
462-
name = self.valueItems[dest.name()].text()
463-
if name.strip() != '':
464-
output = QgsProcessingModelOutput(name, name)
465-
output.setChildId(alg.childId())
466-
output.setChildOutputName(dest.name())
467-
outputs[name] = output
468-
469-
if dest.flags() & QgsProcessingParameterDefinition.FlagIsModelOutput:
470-
if dest.name() not in outputs:
471-
output = QgsProcessingModelOutput(dest.name(), dest.name())
472-
output.setChildId(alg.childId())
473-
output.setChildOutputName(dest.name())
474-
outputs[dest.name()] = output
485+
for output in self._alg.destinationParameterDefinitions():
486+
if not output.flags() & QgsProcessingParameterDefinition.FlagHidden:
487+
wrapper = self.wrappers[output.name()]
488+
489+
if wrapper.isModelOutput():
490+
name = wrapper.modelOutputName()
491+
if name:
492+
model_output = QgsProcessingModelOutput(name, name)
493+
model_output.setChildId(alg.childId())
494+
model_output.setChildOutputName(output.name())
495+
outputs[name] = model_output
496+
else:
497+
val = wrapper.value()
498+
499+
if isinstance(val, QgsProcessingModelChildParameterSource):
500+
val = [val]
501+
502+
alg.addParameterSources(output.name(), val)
503+
504+
if output.flags() & QgsProcessingParameterDefinition.FlagIsModelOutput:
505+
if output.name() not in outputs:
506+
model_output = QgsProcessingModelOutput(output.name(), output.name())
507+
model_output.setChildId(alg.childId())
508+
model_output.setChildOutputName(output.name())
509+
outputs[output.name()] = model_output
475510

476511
alg.setModelOutputs(outputs)
477512

@@ -521,12 +556,6 @@ def switchToCommentTab(self):
521556
self.commentEdit.selectAll()
522557

523558
def setupUi(self):
524-
self.showAdvanced = False
525-
self.wrappers = {}
526-
self.valueItems = {}
527-
self.dependentItems = {}
528-
self.algorithmItem = None
529-
530559
self.mainLayout = QVBoxLayout()
531560
self.mainLayout.setContentsMargins(0, 0, 0, 0)
532561
self.tab = QTabWidget()

‎src/core/processing/models/qgsprocessingmodelalgorithm.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ QVariantMap QgsProcessingModelAlgorithm::parametersForChildAlgorithm( const QgsP
136136
expressionText = QgsExpression::replaceExpressionText( source.expressionText(), &expressionContext );
137137
break;
138138
}
139+
140+
case QgsProcessingModelChildParameterSource::ModelOutput:
141+
break;
139142
}
140143
}
141144

@@ -856,6 +859,7 @@ QMap<QString, QgsProcessingModelAlgorithm::VariableDefinition> QgsProcessingMode
856859
case QgsProcessingModelChildParameterSource::Expression:
857860
case QgsProcessingModelChildParameterSource::ExpressionText:
858861
case QgsProcessingModelChildParameterSource::StaticValue:
862+
case QgsProcessingModelChildParameterSource::ModelOutput:
859863
continue;
860864
};
861865
variables.insert( safeName( name ), VariableDefinition( value, source, description ) );
@@ -901,6 +905,7 @@ QMap<QString, QgsProcessingModelAlgorithm::VariableDefinition> QgsProcessingMode
901905
case QgsProcessingModelChildParameterSource::Expression:
902906
case QgsProcessingModelChildParameterSource::ExpressionText:
903907
case QgsProcessingModelChildParameterSource::StaticValue:
908+
case QgsProcessingModelChildParameterSource::ModelOutput:
904909
continue;
905910

906911
};
@@ -959,6 +964,7 @@ QMap<QString, QgsProcessingModelAlgorithm::VariableDefinition> QgsProcessingMode
959964
case QgsProcessingModelChildParameterSource::Expression:
960965
case QgsProcessingModelChildParameterSource::ExpressionText:
961966
case QgsProcessingModelChildParameterSource::StaticValue:
967+
case QgsProcessingModelChildParameterSource::ModelOutput:
962968
continue;
963969

964970
};

‎src/core/processing/models/qgsprocessingmodelchildparametersource.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ bool QgsProcessingModelChildParameterSource::operator==( const QgsProcessingMode
3939
return mExpression == other.mExpression;
4040
case ExpressionText:
4141
return mExpressionText == other.mExpressionText;
42+
case ModelOutput:
43+
return true;
4244
}
4345
return false;
4446
}
@@ -120,6 +122,9 @@ QVariant QgsProcessingModelChildParameterSource::toVariant() const
120122
case ExpressionText:
121123
map.insert( QStringLiteral( "expression_text" ), mExpressionText );
122124
break;
125+
126+
case ModelOutput:
127+
break;
123128
}
124129
return map;
125130
}
@@ -149,6 +154,9 @@ bool QgsProcessingModelChildParameterSource::loadVariant( const QVariantMap &map
149154
case ExpressionText:
150155
mExpressionText = map.value( QStringLiteral( "expression_text" ) ).toString();
151156
break;
157+
158+
case ModelOutput:
159+
break;
152160
}
153161
return true;
154162
}
@@ -179,6 +187,9 @@ QString QgsProcessingModelChildParameterSource::asPythonCode( const QgsProcessin
179187

180188
case ExpressionText:
181189
return mExpressionText;
190+
191+
case ModelOutput:
192+
return QString();
182193
}
183194
return QString();
184195
}
@@ -222,6 +233,9 @@ QString QgsProcessingModelChildParameterSource::friendlyIdentifier( QgsProcessin
222233

223234
case ExpressionText:
224235
return mExpressionText;
236+
237+
case ModelOutput:
238+
return QString();
225239
}
226240
return QString();
227241
}

‎src/core/processing/models/qgsprocessingmodelchildparametersource.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class CORE_EXPORT QgsProcessingModelChildParameterSource
4343
StaticValue, //!< Parameter value is a static value
4444
Expression, //!< Parameter value is taken from an expression, evaluated just before the algorithm runs
4545
ExpressionText, //!< Parameter value is taken from a text with expressions, evaluated just before the algorithm runs
46+
ModelOutput, //!< Parameter value is linked to an output parameter for the model
4647
};
4748

4849
/**

‎src/gui/processing/models/qgsmodelgraphicsscene.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ QList<QgsModelGraphicsScene::LinkSource> QgsModelGraphicsScene::linkSourcesForPa
395395

396396
case QgsProcessingModelChildParameterSource::StaticValue:
397397
case QgsProcessingModelChildParameterSource::ExpressionText:
398+
case QgsProcessingModelChildParameterSource::ModelOutput:
398399
break;
399400
}
400401
}

‎src/gui/processing/qgsprocessingmodelerparameterwidget.cpp

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "qgsguiutils.h"
2727
#include "qgsexpressioncontext.h"
2828
#include "qgsapplication.h"
29+
#include "qgsfilterlineedit.h"
2930
#include <QHBoxLayout>
3031
#include <QToolButton>
3132
#include <QStackedWidget>
@@ -108,12 +109,25 @@ QgsProcessingModelerParameterWidget::QgsProcessingModelerParameterWidget( QgsPro
108109
hWidget3->setLayout( hLayout3 );
109110
mStackedWidget->addWidget( hWidget3 );
110111

112+
if ( mParameterDefinition->isDestination() )
113+
{
114+
mModelOutputName = new QgsFilterLineEdit();
115+
mModelOutputName->setPlaceholderText( tr( "[Enter name if this is a final result]" ) );
116+
QHBoxLayout *hLayout4 = new QHBoxLayout();
117+
hLayout4->setMargin( 0 );
118+
hLayout4->setContentsMargins( 0, 0, 0, 0 );
119+
hLayout4->addWidget( mModelOutputName );
120+
QWidget *hWidget4 = new QWidget();
121+
hWidget4->setLayout( hLayout4 );
122+
mStackedWidget->addWidget( hWidget4 );
123+
}
124+
111125
hLayout->setMargin( 0 );
112126
hLayout->setContentsMargins( 0, 0, 0, 0 );
113127
hLayout->addWidget( mStackedWidget, 1 );
114128

115129
setLayout( hLayout );
116-
setSourceType( QgsProcessingModelChildParameterSource::StaticValue );
130+
setSourceType( mParameterDefinition->isDestination() ? QgsProcessingModelChildParameterSource::ModelOutput : QgsProcessingModelChildParameterSource::StaticValue );
117131
}
118132

119133
QgsProcessingModelerParameterWidget::~QgsProcessingModelerParameterWidget() = default;
@@ -172,6 +186,23 @@ void QgsProcessingModelerParameterWidget::setWidgetValue( const QList<QgsProcess
172186
}
173187
}
174188

189+
void QgsProcessingModelerParameterWidget::setToModelOutput( const QString &value )
190+
{
191+
if ( mModelOutputName )
192+
mModelOutputName->setText( value );
193+
setSourceType( QgsProcessingModelChildParameterSource::ModelOutput );
194+
}
195+
196+
bool QgsProcessingModelerParameterWidget::isModelOutput() const
197+
{
198+
return currentSourceType() == ModelOutput;
199+
}
200+
201+
QString QgsProcessingModelerParameterWidget::modelOutputName() const
202+
{
203+
return mModelOutputName ? mModelOutputName->text().trimmed() : QString();
204+
}
205+
175206
QVariant QgsProcessingModelerParameterWidget::value() const
176207
{
177208
switch ( currentSourceType() )
@@ -205,6 +236,9 @@ QVariant QgsProcessingModelerParameterWidget::value() const
205236
const QStringList parts = mChildOutputCombo->currentData().toStringList();
206237
return QVariant::fromValue( QgsProcessingModelChildParameterSource::fromChildOutput( parts.value( 0, QString() ), parts.value( 1, QString() ) ) );
207238
}
239+
240+
case ModelOutput:
241+
return mModelOutputName ? ( mModelOutputName->text().trimmed().isEmpty() ? QVariant() : mModelOutputName->text() ) : QVariant();
208242
}
209243

210244
return QVariant::fromValue( QgsProcessingModelChildParameterSource() );
@@ -249,6 +283,14 @@ void QgsProcessingModelerParameterWidget::sourceMenuAboutToShow()
249283

250284
const SourceType currentSource = currentSourceType();
251285

286+
if ( mParameterDefinition->isDestination() )
287+
{
288+
QAction *modelOutputAction = mSourceMenu->addAction( tr( "Model Output" ) );
289+
modelOutputAction->setCheckable( currentSource == ModelOutput );
290+
modelOutputAction->setChecked( currentSource == ModelOutput );
291+
modelOutputAction->setData( QgsProcessingModelChildParameterSource::ModelOutput );
292+
}
293+
252294
if ( mHasStaticWrapper )
253295
{
254296
QAction *fixedValueAction = mSourceMenu->addAction( tr( "Value" ) );
@@ -318,6 +360,14 @@ void QgsProcessingModelerParameterWidget::setSourceType( QgsProcessingModelChild
318360
break;
319361
}
320362

363+
case QgsProcessingModelChildParameterSource::ModelOutput:
364+
{
365+
mSourceButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconModelOutput.svg" ) ) );
366+
mStackedWidget->setCurrentIndex( static_cast< int >( ModelOutput ) );
367+
mSourceButton->setToolTip( tr( "Model Output" ) );
368+
break;
369+
}
370+
321371
case QgsProcessingModelChildParameterSource::ExpressionText:
322372
break;
323373
}
@@ -372,6 +422,7 @@ void QgsProcessingModelerParameterWidget::populateSources( const QStringList &co
372422
case QgsProcessingModelChildParameterSource::StaticValue:
373423
case QgsProcessingModelChildParameterSource::Expression:
374424
case QgsProcessingModelChildParameterSource::ExpressionText:
425+
case QgsProcessingModelChildParameterSource::ModelOutput:
375426
break;
376427
}
377428

‎src/gui/processing/qgsprocessingmodelerparameterwidget.h

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class QgsExpressionLineEdit;
3232
class QgsProcessingModelAlgorithm;
3333
class QgsProcessingParameterWidgetContext;
3434
class QgsProcessingContextGenerator;
35+
class QgsFilterLineEdit;
3536

3637
class QLabel;
3738
class QToolButton;
@@ -117,7 +118,7 @@ class GUI_EXPORT QgsProcessingModelerParameterWidget : public QWidget, public Qg
117118

118119
/**
119120
* Set the expected expression format \a text, which is shown in the expression builder dialog for the widget
120-
* when in the "pre-calculated" expression mode. This is purely a text format and no expression validation is made
121+
* when in the "pre-calculated" ex pression mode. This is purely a text format and no expression validation is made
121122
* against it.
122123
*/
123124
void setExpressionHelpText( const QString &text );
@@ -149,6 +150,33 @@ class GUI_EXPORT QgsProcessingModelerParameterWidget : public QWidget, public Qg
149150
*/
150151
void setWidgetValue( const QList< QgsProcessingModelChildParameterSource > &values );
151152

153+
/**
154+
* Sets the widget to a model output, for destination parameters only.
155+
*
156+
* \see isModelOutput()
157+
* \see modelOutputName()
158+
* \since QGIS 3.14
159+
*/
160+
void setToModelOutput( const QString &value );
161+
162+
/**
163+
* Returns TRUE if the widget is set to the model output mode.
164+
*
165+
* \see setToModelOutput()
166+
* \see modelOutputName()
167+
* \since QGIS 3.14
168+
*/
169+
bool isModelOutput() const;
170+
171+
/**
172+
* Returns the model output name, if isModelOutput() is TRUE.
173+
*
174+
* \see setToModelOutput()
175+
* \see isModelOutput()
176+
* \since QGIS 3.14
177+
*/
178+
QString modelOutputName() const;
179+
152180
/**
153181
* Returns the current value of the parameter.
154182
*
@@ -179,6 +207,7 @@ class GUI_EXPORT QgsProcessingModelerParameterWidget : public QWidget, public Qg
179207
Expression = 1,
180208
ModelParameter = 2,
181209
ChildOutput = 3,
210+
ModelOutput = 4,
182211
};
183212

184213
SourceType currentSourceType() const;
@@ -206,6 +235,7 @@ class GUI_EXPORT QgsProcessingModelerParameterWidget : public QWidget, public Qg
206235
QgsExpressionLineEdit *mExpressionWidget = nullptr;
207236
QComboBox *mModelInputCombo = nullptr;
208237
QComboBox *mChildOutputCombo = nullptr;
238+
QgsFilterLineEdit *mModelOutputName = nullptr;
209239

210240
friend class TestProcessingGui;
211241
};

‎src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5891,6 +5891,7 @@ QWidget *QgsProcessingOutputWidgetWrapper::createWidget()
58915891
switch ( type() )
58925892
{
58935893
case QgsProcessingGui::Standard:
5894+
case QgsProcessingGui::Modeler:
58945895
{
58955896
mOutputWidget = new QgsProcessingLayerOutputDestinationWidget( destParam, false );
58965897
mOutputWidget->setToolTip( parameterDefinition()->toolTip() );
@@ -5903,17 +5904,16 @@ QWidget *QgsProcessingOutputWidgetWrapper::createWidget()
59035904
emit widgetValueHasChanged( this );
59045905
} );
59055906

5906-
if ( destParam->type() == QgsProcessingParameterRasterDestination::typeName() ||
5907-
destParam->type() == QgsProcessingParameterFeatureSink::typeName() ||
5908-
destParam->type() == QgsProcessingParameterVectorDestination::typeName() )
5907+
if ( type() == QgsProcessingGui::Standard
5908+
&& ( destParam->type() == QgsProcessingParameterRasterDestination::typeName() ||
5909+
destParam->type() == QgsProcessingParameterFeatureSink::typeName() ||
5910+
destParam->type() == QgsProcessingParameterVectorDestination::typeName() ) )
59095911
mOutputWidget->addOpenAfterRunningOption();
59105912

59115913
return mOutputWidget;
59125914
}
59135915
case QgsProcessingGui::Batch:
59145916
break;
5915-
case QgsProcessingGui::Modeler:
5916-
break;
59175917
}
59185918

59195919
return nullptr;

‎tests/src/gui/testprocessinggui.cpp

Lines changed: 401 additions & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.