Creating a custom rally list from source using app-catalog on github - rally

I am trying to use the Rally App Catalog and the custom list app
I would like to add an additional selector (item type) and rename some of the headers on the fly. First I just want the sample to run. As it currently stands, it looks broken to me.
When I use the rally-app-builder build function and then paste the result app.html into a custom app I get a blank page with working app settings.
Here the uncompressed version from my initial attempt.
<script src="https://rally1.rallydev.com/apps/2.1/sdk-debug.js"></script>
<!DOCTYPE html>
<html>
<head>
<title>Custom List</title>
<script type="text/javascript" src="/apps/2.1/sdk.js"></script>
<script type="text/javascript">
Rally.onReady(function() {
(function() {
var Ext = window.Ext4 || window.Ext;
var getHiddenFieldConfig = function(name) {
return {
name: name,
xtype: 'rallytextfield',
hidden: true,
handlesEvents: {
typeselected: function(type) {
this.setValue(null);
}
}
};
};
Ext.define('Rally.apps.customlist.Settings', {
singleton: true,
requires: [
'Rally.ui.combobox.FieldComboBox',
'Rally.ui.combobox.ComboBox',
'Rally.ui.CheckboxField'
],
getFields: function(app) {
this.app = app;
return [{
name: 'type',
xtype: 'rallycombobox',
allowBlank: false,
autoSelect: false,
shouldRespondToScopeChange: true,
context: this.app.getContext(),
initialValue: 'HierarchicalRequirement',
storeConfig: {
model: Ext.identityFn('TypeDefinition'),
sorters: [{
property: 'DisplayName'
}],
fetch: ['DisplayName', 'ElementName', 'TypePath', 'Parent', 'UserListable'],
filters: [{
property: 'UserListable',
value: true
}],
autoLoad: false,
remoteSort: false,
remoteFilter: true
},
displayField: 'DisplayName',
valueField: 'TypePath',
listeners: {
select: function(combo) {
this.app.clearFiltersAndSharedViews();
combo.fireEvent('typeselected', combo.getRecord().get('TypePath'), combo.context);
},
scope: this
},
bubbleEvents: ['typeselected'],
readyEvent: 'ready',
handlesEvents: {
projectscopechanged: function(context) {
this.refreshWithNewContext(context);
}
}
}, {
type: 'query'
}, {
name: 'showControls',
xtype: 'rallycheckboxfield',
fieldLabel: 'Show Control Bar'
},
getHiddenFieldConfig('columnNames'),
getHiddenFieldConfig('order')
];
}
});
})();
(function() {
var Ext = window.Ext4 || window.Ext;
Ext.define('Rally.apps.customlist.CustomListApp', {
extend: 'Rally.app.GridBoardApp',
requires: [
'Deft.Promise',
'Rally.apps.customlist.Settings',
'Rally.data.BulkRecordUpdater',
'Rally.data.ModelTypes',
'Rally.data.PreferenceManager',
'Rally.data.util.Sorter',
'Rally.data.wsapi.Filter',
'Rally.ui.gridboard.plugin.GridBoardInlineFilterControl',
'Rally.ui.gridboard.plugin.GridBoardSharedViewControl',
'Rally.ui.notify.Notifier',
'Rally.util.String'
],
disallowedAddNewTypes: ['user', 'userprofile', 'useriterationcapacity', 'testcaseresult', 'task', 'scmrepository', 'project', 'changeset', 'change', 'builddefinition', 'build', 'program'],
orderedAllowedPageSizes: [10, 25, 50, 100, 200],
readOnlyGridTypes: ['build', 'change', 'changeset'],
statePrefix: 'customlist',
allowExpansionStateToBeSaved: false,
isEditable: true,
config: {
defaultSettings: {
showControls: true
}
},
initComponent: function() {
this.appName = 'CustomList-' + this.getAppId();
this.callParent(arguments);
},
getSettingsFields: function() {
return Rally.apps.customlist.Settings.getFields(this);
},
loadModelNames: function() {
this.modelNames = _.compact([this._getTypeSetting()]);
this._setColumnNames(this._getColumnNamesSetting());
return Deft.Promise.when(this.modelNames);
},
addGridBoard: function() {
this.callParent(arguments);
if (!this.getSetting('showControls')) {
this.gridboard.getHeader().hide();
}
},
loadGridBoard: function() {
if (_.isEmpty(this.modelNames)) {
Ext.defer(function() {
this.fireEvent('settingsneeded', this);
this.publishComponentReady();
}, 1, this);
} else {
this.enableAddNew = this._shouldEnableAddNew();
this.enableRanking = this._shouldEnableRanking();
this.callParent(arguments);
}
},
getGridConfig: function() {
var config = _.merge(this.callParent(arguments), {
allColumnsStateful: true,
enableEditing: !_.contains(this.readOnlyGridTypes, this._getTypeSetting().toLowerCase()),
listeners: {
beforestaterestore: this._onBeforeGridStateRestore,
beforestatesave: this._onBeforeGridStateSave,
scope: this
},
pagingToolbarCfg: {
hidden: !this.getSetting('showControls'),
pageSizes: this.orderedAllowedPageSizes
}
});
var invalidQueryFilters = Rally.util.Filter.findInvalidSubFilters(this._getQueryFilter(), this.models);
if (invalidQueryFilters.length) {
config.store.on('beforeload', function(store) {
Ext.defer(function() {
store.fireEvent('load', store, store.getRootNode(), [], true);
}, 1);
return false;
});
this._showInvalidQueryMessage(config, _.map(invalidQueryFilters, function(filter) {
return 'Could not find the attribute "' + filter.property.split('.')[0] + '" on type "' + this.models[0].displayName + '" in the query segment "' + filter.toString() + '"';
}, this));
}
return config;
},
getColumnCfgs: function() {
return _.union(this.callParent(arguments), _.isEmpty(this.columnNames) && this.enableRanking ? ['DragAndDropRank'] : []);
},
getFilterControlConfig: function() {
return _.merge(this.callParent(arguments), {
listeners: {
beforestaterestore: {
fn: this._onBeforeFilterButtonStateRestore,
scope: this
}
}
});
},
getGridBoardCustomFilterControlConfig: function() {
var context = this.getContext();
var isArtifactModel = this.models[0].isArtifact();
var blackListFields = isArtifactModel ? ['ModelType', 'PortfolioItemType'] : ['ArtifactSearch', 'ModelType'];
var whiteListFields = isArtifactModel ? ['Milestones', 'Tags'] : [];
if (this.models[0].isProject()) {
blackListFields.push('SchemaVersion');
} else if (this.models[0].isRelease()) {
blackListFields.push('ChildrenPlannedVelocity', 'Version');
}
var config = {
ptype: 'rallygridboardinlinefiltercontrol',
inlineFilterButtonConfig: {
stateful: true,
stateId: context.getScopedStateId('custom-list-inline-filter'),
legacyStateIds: [
this.getScopedStateId('owner-filter'),
this.getScopedStateId('custom-filter-button')
],
filterChildren: true,
inlineFilterPanelConfig: {
quickFilterPanelConfig: {
defaultFields: isArtifactModel ? ['ArtifactSearch', 'Owner'] : [],
addQuickFilterConfig: {
blackListFields: blackListFields,
whiteListFields: whiteListFields
}
},
advancedFilterPanelConfig: {
advancedFilterRowsConfig: {
propertyFieldConfig: {
blackListFields: blackListFields,
whiteListFields: whiteListFields
}
}
}
}
}
};
if (isArtifactModel) {
config.inlineFilterButtonConfig.modelNames = this.modelNames;
} else {
config.inlineFilterButtonConfig.model = this.models[0];
}
return config;
},
getSharedViewConfig: function() {
var context = this.getContext();
return {
ptype: 'rallygridboardsharedviewcontrol',
sharedViewConfig: {
stateful: true,
stateId: context.getScopedStateId('custom-list-shared-view'),
enableUrlSharing: this.isFullPageApp !== false
}
};
},
getGridBoardConfig: function() {
var config = this.callParent(arguments);
return _.merge(config, {
listeners: {
viewchange: function() {
this.loadGridBoard();
},
filterchange: function() {
this.gridboard.getGridOrBoard().noDataPrimaryText = undefined;
this.gridboard.getGridOrBoard().noDataSecondaryText = undefined;
},
scope: this
}
});
},
onTreeGridReady: function(grid) {
if (grid.store.getTotalCount() > 10) {
this.gridboard.down('#pagingToolbar').show();
}
this.callParent(arguments);
},
getGridStoreConfig: function() {
var sorters = this._getValidSorters(Rally.data.util.Sorter.sorters(this.getSetting('order')));
if (_.isEmpty(sorters)) {
var rankField = this.getContext().getWorkspace().WorkspaceConfiguration.DragDropRankingEnabled ? 'DragAndDropRank' : 'Rank';
var defaultSort = Rally.data.ModelTypes.areArtifacts(this.modelNames) ? rankField : Rally.data.util.Sorter.getDefaultSort(this.modelNames[0]);
sorters = Rally.data.util.Sorter.sorters(defaultSort);
}
return {
listeners: {
warning: {
fn: this._onGridStoreWarning,
scope: this
}
},
pageSize: 10,
sorters: sorters
};
},
getAddNewConfig: function() {
var config = {
minWidth: 700,
openEditorAfterAddFailure: false,
margin: 0
};
if (!this.getContext().isFeatureEnabled('F6971_REACT_DASHBOARD_PANELS')) {
config.disableAddButton = this.appContainer.slug === 'incompletestories';
}
return _.merge(this.callParent(arguments), config);
},
getFieldPickerConfig: function() {
return _.merge(this.callParent(arguments), {
buttonConfig: {
disabled: !this._userHasPermissionsToEditPanelSettings()
},
gridAlwaysSelectedValues: function() {
return [];
},
gridFieldBlackList: this._getTypeSetting().toLowerCase() === 'task' ? ['Rank'] : []
});
},
getPermanentFilters: function() {
return this._getQueryFilter().concat(this._getTimeboxScopeFilter()).concat(this._getProjectFilter());
},
onTimeboxScopeChange: function() {
this.callParent(arguments);
this.loadGridBoard();
},
clearFiltersAndSharedViews: function() {
var context = this.getContext();
if (this.gridboard) {
this.gridboard.down('rallyinlinefilterpanel').clear();
this.gridboard.down('rallysharedviewcombobox').reset();
}
Ext.create('Rally.data.wsapi.Store', {
model: Ext.identityFn('preference'),
autoLoad: true,
filters: [{
property: 'AppId',
value: context.getAppId()
}, {
property: 'Type',
value: 'View'
}, {
property: 'Workspace',
value: context.getWorkspace()._ref
}],
context: context.getDataContext(),
listeners: {
load: function(store, records) {
if (!_.isEmpty(records)) {
var batchStore = Ext.create('Rally.data.wsapi.batch.Store', {
requester: this,
data: records
});
batchStore.removeAll();
batchStore.sync();
}
store.destroyStore();
},
scope: this
}
});
},
_getTypeSetting: function() {
return this.getSetting('type') || this.getSetting('url');
},
_getColumnNamesSetting: function() {
return this.getSetting('columnNames') ||
(this.getSetting('fetch') || '').split(',');
},
_getQueryFilter: function() {
var query = new Ext.Template(this.getSetting('query')).apply({
projectName: this.getContext().getProject().Name,
projectOid: this.getContext().getProject().ObjectID,
user: this.getContext().getUser()._ref
});
if (query) {
try {
return [Rally.data.wsapi.Filter.fromQueryString(query)];
} catch (e) {
Rally.ui.notify.Notifier.showError({
message: e.message
});
}
}
return [];
},
_getProjectFilter: function() {
return this.modelNames[0].toLowerCase() === 'milestone' ? [
Rally.data.wsapi.Filter.or([{
property: 'Projects',
operator: 'contains',
value: this.getContext().getProjectRef()
}, {
property: 'TargetProject',
operator: '=',
value: null
}])
] : [];
},
_getTimeboxScopeFilter: function() {
var timeboxScope = this.getContext().getTimeboxScope();
var hasTimeboxField = timeboxScope && _.any(this.models, timeboxScope.isApplicable, timeboxScope);
return hasTimeboxField ? [timeboxScope.getQueryFilter()] : [];
},
_shouldEnableAddNew: function() {
return !_.contains(this.disallowedAddNewTypes, this._getTypeSetting().toLowerCase());
},
_shouldEnableRanking: function() {
return this._getTypeSetting().toLowerCase() !== 'task';
},
_setColumnNames: function(columnNames) {
this.columnNames = _.compact(_.isString(columnNames) ? columnNames.split(',') : columnNames);
},
_onBeforeFilterButtonStateRestore: function(filterButton, state) {
if (state && state.filters && state.filters.length) {
var stateFilters = _.map(state.filters, function(filterStr) {
return Rally.data.wsapi.Filter.fromQueryString(filterStr);
});
var validFilters = Rally.util.Filter.removeNonapplicableTypeSpecificFilters(stateFilters, this.models);
state.filters = _.invoke(validFilters, 'toString');
}
},
_hasViewSelected: function() {
var sharedViewConfig = this.getSharedViewConfig().sharedViewConfig;
if (sharedViewConfig && sharedViewConfig.stateId) {
var value = (Ext.state.Manager.get(sharedViewConfig.stateId) || {}).value;
return !_.isEmpty(value);
}
return false;
},
_onBeforeGridStateRestore: function(grid, state) {
if (!state) {
return;
}
if (state.columns) {
var appScopedColumnNames = this._getValidUuids(grid, this.getColumnCfgs());
var userScopedColumnNames = this._getValidUuids(grid, state.columns);
if (this._hasViewSelected()) {
state.columns = userScopedColumnNames;
} else {
// Get the columns that are present in the app scope and not in the user scope
var differingColumns = _.difference(appScopedColumnNames, userScopedColumnNames);
// If there are columns in the app scope that are not in the
// user scope, append them to the user scope to preserve
// user scope column order
if (differingColumns.length > 0) {
state.columns = state.columns.concat(differingColumns);
}
// Filter out any columns that are in the user scope that are not in the
// app scope
state.columns = _.filter(state.columns, function(column) {
return _.contains(appScopedColumnNames, _.isObject(column) ? column.dataIndex : column);
}, this);
}
}
if (state.sorters) {
state.sorters = this._getValidSorters(state.sorters);
if (_.isEmpty(state.sorters)) {
delete state.sorters;
}
}
},
_getValidUuids: function(grid, columns) {
return _.reduce(columns, function(result, column) {
var dataIndex = this._getColumnDataIndex(column);
var field = this._getModelField(grid, dataIndex);
if (field) {
result.push(dataIndex);
}
return result;
}, [], this);
},
_getModelField: function(grid, dataIndex) {
return grid.getModels()[0].getField(dataIndex);
},
_getColumnDataIndex: function(column) {
return _.isObject(column) ? column.dataIndex : column;
},
_onBeforeGridStateSave: function(grid, state) {
var newColumnNames = this._getColumnNamesFromState(state);
if (!_.isEmpty(newColumnNames)) {
this._setColumnNames(newColumnNames);
if (this._userHasPermissionsToEditPanelSettings()) {
this.updateSettingsValues({
settings: {
columnNames: newColumnNames.join(',')
}
});
}
}
},
_onGridStoreWarning: function(store, warnings, operation) {
var couldNotParseWarnings = _.filter(warnings, function(warning) {
return Rally.util.String.startsWith(warning, 'Could not parse ');
});
if (couldNotParseWarnings.length) {
_.assign(operation.resultSet, {
count: 0,
records: [],
total: 0,
totalRecords: 0
});
this._showInvalidQueryMessage(this.gridboard.getGridOrBoard(), couldNotParseWarnings);
}
},
_showInvalidQueryMessage: function(gridOrGridConfig, secondaryTextStrings) {
gridOrGridConfig.noDataPrimaryText = 'Invalid Query';
gridOrGridConfig.noDataSecondaryText = _.map(secondaryTextStrings, function(str) {
return '<div>' + str + '</div>';
}).join('');
},
_getValidSorters: function(sorters) {
return _.filter(sorters, function(sorter) {
return _.any(this.models, function(model) {
var field = model.getField(sorter.property);
return field && field.sortable;
});
}, this);
},
_userHasPermissionsToEditPanelSettings: function() {
return this.isEditable;
},
_getColumnNamesFromState: function(state) {
return _(state && state.columns).map(function(newColumn) {
return _.isObject(newColumn) ? newColumn.dataIndex : newColumn;
}).compact().value();
}
});
})();
Rally.launchApp('Rally.apps.customlist.CustomListApp', {
name: "Custom List",
parentRepos: ""
});
});
</script>
</head>
<body>
</body>
</html>

Ok, I got results by commenting out the following lines in getAddNewConfig:
getAddNewConfig: function () {
var config = {
minWidth: 700,
openEditorAfterAddFailure: false,
margin: 0
};
//if(!this.getContext().isFeatureEnabled('F6971_REACT_DASHBOARD_PANELS')) {
// config.disableAddButton = this.appContainer.slug === 'incompletestories';
//}
return _.merge(this.callParent(arguments), config);
},
It seems that 'this.appContainer.slug' is undefined for me.

Here's the final working CustomListApp.js generated from the comment stream in the answer above. The main tweaks were deleting some old code checking a feature toggle and giving it a default type to display (defect in this case) in the defaultSettings block.
(function() {
var Ext = window.Ext4 || window.Ext;
Ext.define('Rally.apps.customlist.CustomListApp', {
extend: 'Rally.app.GridBoardApp',
requires: [
'Deft.Promise',
'Rally.apps.customlist.Settings',
'Rally.data.BulkRecordUpdater',
'Rally.data.ModelTypes',
'Rally.data.PreferenceManager',
'Rally.data.util.Sorter',
'Rally.data.wsapi.Filter',
'Rally.ui.gridboard.plugin.GridBoardInlineFilterControl',
'Rally.ui.gridboard.plugin.GridBoardSharedViewControl',
'Rally.ui.notify.Notifier',
'Rally.util.String'
],
disallowedAddNewTypes: ['user', 'userprofile', 'useriterationcapacity', 'testcaseresult', 'task', 'scmrepository', 'project', 'changeset', 'change', 'builddefinition', 'build', 'program'],
orderedAllowedPageSizes: [10, 25, 50, 100, 200],
readOnlyGridTypes: ['build', 'change', 'changeset'],
statePrefix: 'customlist',
allowExpansionStateToBeSaved: false,
isEditable: true,
config: {
defaultSettings: {
showControls: true,
type: 'defect' //important to include a default type to display
}
},
initComponent: function () {
this.appName = 'CustomList-' + this.getAppId();
if (this.defaultSettings.url) {
Ext.apply(this.defaultSettings, { type: this.defaultSettings.url });
}
this.callParent(arguments);
},
getSettingsFields: function() {
return Rally.apps.customlist.Settings.getFields(this);
},
loadModelNames: function () {
this.modelNames = _.compact(this.getTypeSetting());
this._setColumnNames(this._getColumnNamesSetting());
return Deft.Promise.when(this.modelNames);
},
addGridBoard: function () {
this.callParent(arguments);
if (!this.getSetting('showControls')) {
this.gridboard.getHeader().hide();
}
},
loadGridBoard: function () {
if (_.isEmpty(this.modelNames)) {
Ext.defer(function () {
this.fireEvent('settingsneeded', this);
this.publishComponentReady();
}, 1, this);
} else {
this.enableAddNew = this._shouldEnableAddNew();
this.enableRanking = this._shouldEnableRanking();
this.callParent(arguments);
}
},
getGridConfig: function () {
var config = _.merge(this.callParent(arguments), {
allColumnsStateful: true,
enableEditing: _.intersection(this.readOnlyGridTypes, this.getTypeSetting()).length === 0,
listeners: {
beforestaterestore: this._onBeforeGridStateRestore,
beforestatesave: this._onBeforeGridStateSave,
scope: this
},
pagingToolbarCfg: {
hidden: !this.getSetting('showControls'),
pageSizes: this.orderedAllowedPageSizes
}
});
var invalidQueryFilters = Rally.util.Filter.findInvalidSubFilters(this._getQueryFilter(), this.models);
if (invalidQueryFilters.length) {
config.store.on('beforeload', function (store) {
Ext.defer(function () {
store.fireEvent('load', store, store.getRootNode(), [], true);
}, 1);
return false;
});
this._showInvalidQueryMessage(config, _.map(invalidQueryFilters, function (filter) {
return 'Could not find the attribute "'+ filter.property.split('.')[0] +'" on type "'+ this.models[0].displayName +'" in the query segment "'+ filter.toString() + '"';
}, this));
}
return config;
},
getColumnCfgs: function() {
return _.union(this.callParent(arguments), _.isEmpty(this.columnNames) && this.enableRanking ? ['DragAndDropRank'] : []);
},
getFilterControlConfig: function () {
return _.merge(this.callParent(arguments), {
listeners: {
beforestaterestore: {
fn: this._onBeforeFilterButtonStateRestore,
scope: this
}
}
});
},
getGridBoardCustomFilterControlConfig: function() {
var context = this.getContext();
var isArtifactModel = this.models[0].isArtifact();
var blackListFields = isArtifactModel ? ['ModelType', 'PortfolioItemType', 'LastResult'] : ['ArtifactSearch', 'ModelType'];
var whiteListFields = isArtifactModel ? ['Milestones', 'Tags'] : [];
if (this.models[0].isProject()) {
blackListFields.push('SchemaVersion');
} else if (this.models[0].isRelease()) {
blackListFields.push('ChildrenPlannedVelocity', 'Version');
}
var config = {
ptype: 'rallygridboardinlinefiltercontrol',
inlineFilterButtonConfig: {
stateful: true,
stateId: context.getScopedStateId('custom-list-inline-filter'),
legacyStateIds: [
this.getScopedStateId('owner-filter'),
this.getScopedStateId('custom-filter-button')
],
filterChildren: true,
inlineFilterPanelConfig: {
quickFilterPanelConfig: {
defaultFields: isArtifactModel ? ['ArtifactSearch', 'Owner'] : [],
addQuickFilterConfig: {
blackListFields: blackListFields,
whiteListFields: whiteListFields
}
},
advancedFilterPanelConfig: {
advancedFilterRowsConfig: {
propertyFieldConfig: {
blackListFields: blackListFields,
whiteListFields: whiteListFields
}
}
}
}
}
};
if (isArtifactModel) {
config.inlineFilterButtonConfig.modelNames = this.modelNames;
} else {
config.inlineFilterButtonConfig.model = this.models[0];
}
return config;
},
getSharedViewConfig: function() {
var context = this.getContext();
return {
ptype: 'rallygridboardsharedviewcontrol',
sharedViewConfig: {
stateful: true,
stateId: context.getScopedStateId('custom-list-shared-view'),
enableUrlSharing: this.isFullPageApp !== false
}
};
},
getGridBoardConfig: function () {
var config = this.callParent(arguments);
return _.merge(config, {
listeners: {
viewchange: function() {
this.loadGridBoard();
},
filterchange: function() {
this.gridboard.getGridOrBoard().noDataPrimaryText = undefined;
this.gridboard.getGridOrBoard().noDataSecondaryText = undefined;
},
scope: this
}
});
},
onTreeGridReady: function (grid) {
if (grid.store.getTotalCount() > 10) {
this.gridboard.down('#pagingToolbar').show();
}
this.callParent(arguments);
},
getGridStoreConfig: function () {
var sorters = this._getValidSorters(Rally.data.util.Sorter.sorters(this.getSetting('order')));
if (_.isEmpty(sorters)) {
var rankField = this.getContext().getWorkspace().WorkspaceConfiguration.DragDropRankingEnabled ? 'DragAndDropRank' : 'Rank';
var defaultSort = Rally.data.ModelTypes.areArtifacts(this.modelNames) ? rankField : Rally.data.util.Sorter.getDefaultSort(this.modelNames[0]);
sorters = Rally.data.util.Sorter.sorters(defaultSort);
}
return {
listeners: {
warning: {
fn: this._onGridStoreWarning,
scope: this
}
},
pageSize: 10,
sorters: sorters
};
},
getAddNewConfig: function () {
var config = {
minWidth: 700,
openEditorAfterAddFailure: false,
margin: 0
};
return _.merge(this.callParent(arguments), config);
},
getFieldPickerConfig: function () {
return _.merge(this.callParent(arguments), {
buttonConfig: {
disabled: !this._userHasPermissionsToEditPanelSettings()
},
gridAlwaysSelectedValues: function () { return []; },
gridFieldBlackList: this._shouldEnableRanking() ? [] : ['Rank']
});
},
getPermanentFilters: function () {
return this._getQueryFilter().concat(this._getTimeboxScopeFilter()).concat(this._getProjectFilter());
},
onTimeboxScopeChange: function() {
this.callParent(arguments);
this.loadGridBoard();
},
clearFiltersAndSharedViews: function() {
var context = this.getContext();
if (this.gridboard) {
this.gridboard.down('rallyinlinefilterpanel').clear();
this.gridboard.down('rallysharedviewcombobox').reset();
}
Ext.create('Rally.data.wsapi.Store', {
model: Ext.identityFn('preference'),
autoLoad: true,
filters: [
{property: 'AppId', value: context.getAppId()},
{property: 'Type', value: 'View'},
{property: 'Workspace', value: context.getWorkspace()._ref}
],
context: context.getDataContext(),
listeners: {
load: function(store, records) {
if(!_.isEmpty(records)) {
var batchStore = Ext.create('Rally.data.wsapi.batch.Store', {
requester: this,
data: records
});
batchStore.removeAll();
batchStore.sync();
}
store.destroyStore();
},
scope: this
}
});
},
getTypeSetting: function() {
return (this.getSetting('type') || this.getSetting('url') || '').toLowerCase().split(',');
},
_getColumnNamesSetting: function() {
return this.getSetting('columnNames') ||
(this.getSetting('fetch') || '').split(',');
},
_getQueryFilter: function () {
var query = new Ext.Template(this.getSetting('query')).apply({
projectName: this.getContext().getProject().Name,
projectOid: this.getContext().getProject().ObjectID,
user: this.getContext().getUser()._ref
});
if (query) {
try {
return [ Rally.data.wsapi.Filter.fromQueryString(query) ];
} catch(e) {
Rally.ui.notify.Notifier.showError({ message: e.message });
}
}
return [];
},
_getProjectFilter: function () {
return this.modelNames[0].toLowerCase() === 'milestone' ? [
Rally.data.wsapi.Filter.or([
{ property: 'Projects', operator: 'contains', value: this.getContext().getProjectRef() },
{ property: 'TargetProject', operator: '=', value: null }
])
] : [];
},
_getTimeboxScopeFilter: function () {
var timeboxScope = this.getContext().getTimeboxScope();
var hasTimeboxField = timeboxScope && _.any(this.models, timeboxScope.isApplicable, timeboxScope);
return hasTimeboxField ? [ timeboxScope.getQueryFilter() ] : [];
},
_shouldEnableAddNew: function() {
return _.intersection(this.disallowedAddNewTypes, this.getTypeSetting()).length === 0;
},
_shouldEnableRanking: function() {
return !_.contains(this.getTypeSetting(), 'task');
},
_setColumnNames: function (columnNames) {
this.columnNames = _.compact(_.isString(columnNames) ? columnNames.split(',') : columnNames);
},
_onBeforeFilterButtonStateRestore: function (filterButton, state) {
if (state && state.filters && state.filters.length) {
var stateFilters = _.map(state.filters, function (filterStr) {
return Rally.data.wsapi.Filter.fromQueryString(filterStr);
});
var validFilters = Rally.util.Filter.removeNonapplicableTypeSpecificFilters(stateFilters, this.models);
state.filters = _.invoke(validFilters, 'toString');
}
},
_hasViewSelected: function() {
var sharedViewConfig = this.getSharedViewConfig().sharedViewConfig;
if (sharedViewConfig && sharedViewConfig.stateId) {
var value = (Ext.state.Manager.get(sharedViewConfig.stateId) || {}).value;
return !_.isEmpty(value);
}
return false;
},
_onBeforeGridStateRestore: function (grid, state) {
if (!state) {
return;
}
if (state.columns) {
var appScopedColumnNames = this._getValidUuids(grid, this.getColumnCfgs());
var userScopedColumnNames = this._getValidUuids(grid, state.columns);
if (this._hasViewSelected()) {
state.columns = userScopedColumnNames;
} else {
// Get the columns that are present in the app scope and not in the user scope
var differingColumns = _.difference(appScopedColumnNames, userScopedColumnNames);
// If there are columns in the app scope that are not in the
// user scope, append them to the user scope to preserve
// user scope column order
if (differingColumns.length > 0) {
state.columns = state.columns.concat(differingColumns);
}
// Filter out any columns that are in the user scope that are not in the
// app scope
state.columns = _.filter(state.columns, function (column) {
return _.contains(appScopedColumnNames, _.isObject(column) ? column.dataIndex : column);
}, this);
}
}
if (state.sorters) {
state.sorters = this._getValidSorters(state.sorters);
if (_.isEmpty(state.sorters)) {
delete state.sorters;
}
}
},
_getValidUuids: function(grid, columns) {
return _.reduce(columns, function(result, column) {
var dataIndex = this._getColumnDataIndex(column);
var field = this._getModelField(grid, dataIndex);
if (field) {
result.push(dataIndex);
}
return result;
}, [], this);
},
_getModelField: function(grid, dataIndex) {
return grid.getModels()[0].getField(dataIndex);
},
_getColumnDataIndex: function(column) {
return _.isObject(column) ? column.dataIndex : column;
},
_onBeforeGridStateSave: function (grid, state) {
var newColumnNames = this._getColumnNamesFromState(state);
if (!_.isEmpty(newColumnNames)) {
this._setColumnNames(newColumnNames);
if (this._userHasPermissionsToEditPanelSettings()) {
this.updateSettingsValues({
settings: {
columnNames: newColumnNames.join(',')
}
});
}
}
},
_onGridStoreWarning: function(store, warnings, operation) {
var couldNotParseWarnings = _.filter(warnings, function(warning){
return Rally.util.String.startsWith(warning, 'Could not parse ');
});
if(couldNotParseWarnings.length) {
_.assign(operation.resultSet, {
count: 0,
records: [],
total: 0,
totalRecords: 0
});
this._showInvalidQueryMessage(this.gridboard.getGridOrBoard(), couldNotParseWarnings);
}
},
_showInvalidQueryMessage: function(gridOrGridConfig, secondaryTextStrings) {
gridOrGridConfig.noDataPrimaryText = 'Invalid Query';
gridOrGridConfig.noDataSecondaryText = _.map(secondaryTextStrings, function(str){
return '<div>' + str + '</div>';
}).join('');
},
_getValidSorters: function (sorters) {
return _.filter(sorters, function (sorter) {
return _.any(this.models, function (model) {
var field = model.getField(sorter.property);
return field && field.sortable;
});
}, this);
},
_userHasPermissionsToEditPanelSettings: function () {
return this.isEditable;
},
_getColumnNamesFromState: function (state) {
return _(state && state.columns).map(function (newColumn) {
return _.isObject(newColumn) ? newColumn.dataIndex : newColumn;
}).compact().value();
}
});
})();

Related

vuejs store localStorage set object

I have a theme config store, and I set "localStorage" on this store, I can read it the same way. but I'm recording them one by one, is it possible to send them all as objects instead, if possible, please give an example.
how do i set it on the store
I'm a little newbie in storage, please comment accordingly.
export const $themeConfig = {
app: {
appName: 'Name',
appLogoImage: '',
},
layout: {
skin: 'light',
type: 'vertical',
contentWidth: 'full',
menu: {
hidden: false,
isCollapsed: false,
},
navbar: {
type: 'floating',
backgroundColor: 'dark',
},
footer: {
type: 'static',
},
customizer: true,
},
}
import {$themeConfig} from '../../themeConfig'
export default {
namespaced: true,
state: {
layout: {
skin: localStorage.getItem('themeConfig-skin') || $themeConfig.layout.skin,
type: localStorage.getItem('themeConfig-menuLayout') || $themeConfig.layout.type,
contentWidth: localStorage.getItem('themeConfig-contentWidth') || $themeConfig.layout.contentWidth,
menu: {
hidden: localStorage.getItem('themeConfig-menuHidden') || $themeConfig.layout.menu.hidden,
},
navbar: {
type: localStorage.getItem('themeConfig-navbarType') || $themeConfig.layout.navbar.type,
backgroundColor: localStorage.getItem('themeConfig-navbarBG') || $themeConfig.layout.navbar.backgroundColor,
},
footer: {
type: localStorage.getItem('themeConfig-footerType') || $themeConfig.layout.footer.type,
},
},
},
getters: {},
mutations: {
UPDATE_SKIN(state, skin) {
state.layout.skin = skin
localStorage.setItem('themeConfig-skin', skin)
if (skin === 'dark') document.body.classList.add('dark-layout')
else if (document.body.className.match('dark-layout')) document.body.classList.remove('dark-layout')
},
UPDATE_LAYOUT_TYPE(state, val) {
localStorage.setItem('themeConfig-menuLayout', val)
state.layout.type = val
},
UPDATE_CONTENT_WIDTH(state, val) {
localStorage.setItem('themeConfig-contentWidth', val)
state.layout.contentWidth = val
},
UPDATE_NAV_MENU_HIDDEN(state, val) {
localStorage.setItem('themeConfig-menuHidden', val)
state.layout.menu.hidden = val
},
UPDATE_NAVBAR_CONFIG(state, obj) {
if (obj.backgroundColor)
localStorage.setItem('themeConfig-navbarBG', obj.backgroundColor)
else
localStorage.setItem('themeConfig-navbarType', obj.type)
Object.assign(state.layout.navbar, obj)
},
UPDATE_FOOTER_CONFIG(state, obj) {
if (obj.type)
localStorage.setItem('themeConfig-footerType', obj.type)
else
localStorage.setItem('themeConfig-footerType', obj.type)
Object.assign(state.layout.footer, obj)
},
},
actions: {},
}
You can create helper functions like:
function updateThemeConfig (key, data) {
let state = localStorage.getItem('themeConfig');
if(state){
state = JSON.parse(state);
state[key] = data;
} else {
state = {
[key]: data;
}
}
localStorage.setItem('themeConfig', JSON.stringify(state))
}
function getThemeConfig (key) {
let state = localStorage.getItem('themeConfig');
if(state) {
state = JSON.parse(state);
return state[key];
}
return;
}
Then use it like this:
footer: {
type: getThemeConfig('footerType') || $themeConfig.layout.footer.type,
},
...
UPDATE_LAYOUT_TYPE(state, val) {
updateThemeConfig('menuLayout', val);
state.layout.type = val
},

Definition for rule 'vue/require-prop-types' was not found vue/require-prop-types

I'm currently struggling with this issue when trying to build the administration on my shopware6 instance.
I am not sure to understand what it's expecting with this error
Definition for rule 'vue/require-prop-types' was not found vue/require-prop-types
This is pointing the line 22 which is :
props: {
Here's the index.js file :
import './custom-entity-single-select.scss';
import template from './custom-entity-single-select.html.twig';
const { Component, Mixin, Utils } = Shopware;
const { Criteria, EntityCollection } = Shopware.Data;
const { debounce, get } = Shopware.Utils;
Component.register('custom-entity-single-select', {
template,
inject: { repositoryFactory: 'repositoryFactory', feature: 'feature' },
mixins: [
Mixin.getByName('remove-api-error'),
],
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
required: false,
},
highlightSearchTerm: {
type: Boolean,
required: false,
default: true,
},
placeholder: {
type: String,
required: false,
default: '',
},
resetOption: {
type: String,
required: false,
default: '',
},
labelProperty: {
type: [String, Array],
required: false,
default: 'name',
},
labelCallback: {
type: Function,
required: false,
default: null,
},
entity: {
required: true,
type: String,
},
resultLimit: {
type: Number,
required: false,
default: 25,
},
criteria: {
type: Object,
required: false,
default() {
return (new Criteria(1, this.resultLimit)).getAssociation('stateMachine').addFilter(Criteria.equals('state_machine_state.stateMachine.technicalName', 'order_transaction.state'));
},
},
context: {
type: Object,
required: false,
default() {
return Shopware.Context.api;
},
},
disableAutoClose: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
searchTerm: '',
isExpanded: false,
resultCollection: null,
singleSelection: null,
isLoading: false,
// used to track if an item was selected before closing the result list
itemRecentlySelected: false,
lastSelection: null,
};
},
computed: {
inputClasses() {
return {
'is--expanded': this.isExpanded,
};
},
selectionTextClasses() {
return {
'is--placeholder': !this.singleSelection,
};
},
repository() {
return this.repositoryFactory.create(this.entity);
},
/**
* #returns {EntityCollection}
*/
results() {
return this.resultCollection;
},
},
watch: {
value(value) {
// No need to fetch again when the new value is the last one we selected
if (this.lastSelection && this.value === this.lastSelection.id) {
this.singleSelection = this.lastSelection;
this.lastSelection = null;
return;
}
if (value === '' || value === null) {
this.singleSelection = null;
return;
}
this.loadSelected();
},
},
created() {
this.createdComponent();
},
methods: {
createdComponent() {
this.loadSelected();
},
/**
* Fetches the selected entity from the server
*/
loadSelected() {
if (!this.value) {
if (this.resetOption) {
this.singleSelection = {
id: null,
name: this.resetOption,
};
}
return Promise.resolve();
}
this.isLoading = true;
return this.repository.get(this.value, { ...this.context, inheritance: true }, this.criteria).then((item) => {
this.criteria.setIds([]);
this.singleSelection = item;
this.isLoading = false;
return item;
});
},
createCollection(collection) {
return new EntityCollection(collection.source, collection.entity, collection.criteria);
},
isSelected(item) {
return item.id === this.value;
},
debouncedSearch: debounce(function updateSearchTerm() {
this.search();
}, 400),
search() {
if (this.criteria.term === this.searchTerm) {
return Promise.resolve();
}
this.criteria.setPage(1);
this.criteria.setLimit(this.resultLimit);
this.criteria.setTerm(this.searchTerm);
this.resultCollection = null;
const searchPromise = this.loadData().then(() => {
this.resetActiveItem();
});
this.$emit('search', searchPromise);
return searchPromise;
},
paginate() {
if (!this.resultCollection || this.resultCollection.total < this.criteria.page * this.criteria.limit) {
return;
}
this.criteria.setPage(this.criteria.page + 1);
this.loadData();
},
loadData() {
this.isLoading = true;
return this.repository.search(this.criteria, { ...this.context, inheritance: true }).then((result) => {
this.displaySearch(result);
this.isLoading = false;
return result;
});
},
displaySearch(result) {
if (!this.resultCollection) {
this.resultCollection = result;
} else {
result.forEach(item => {
// Prevent duplicate entries
if (!this.resultCollection.has(item.id)) {
this.resultCollection.push(item);
}
});
}
if (this.resetOption) {
if (!this.resultCollection.has(null)) {
this.resultCollection.unshift({
id: null,
name: this.resetOption,
});
}
}
},
displayLabelProperty(item) {
if (typeof this.labelCallback === 'function') {
return this.labelCallback(item);
}
const labelProperties = [];
if (Array.isArray(this.labelProperty)) {
labelProperties.push(...this.labelProperty);
} else {
labelProperties.push(this.labelProperty);
}
return labelProperties.map(labelProperty => {
return this.getKey(item, labelProperty) || this.getKey(item, `translated.${labelProperty}`);
}).join(' ');
},
onSelectExpanded() {
this.isExpanded = true;
// Always start with a fresh list when opening the result list
this.criteria.setPage(1);
this.criteria.setLimit(this.resultLimit);
this.criteria.setTerm('');
this.resultCollection = null;
this.loadData().then(() => {
this.resetActiveItem();
});
// Get the search text of the selected item as prefilled value
this.searchTerm = this.tryGetSearchText(this.singleSelection);
this.$nextTick(() => {
this.$refs.customSelectInput.select();
this.$refs.customSelectInput.focus();
});
},
tryGetSearchText(option) {
if (typeof this.labelCallback === 'function') {
return this.labelCallback(option);
}
let searchText = this.getKey(option, this.labelProperty, '');
if (!searchText) {
searchText = this.getKey(option, `translated.${this.labelProperty}`, '');
}
return searchText;
},
onSelectCollapsed() {
// Empty the selection if the search term is empty
if (this.searchTerm === '' && !this.itemRecentlySelected) {
this.clearSelection();
}
this.$refs.customSelectInput.blur();
this.searchTerm = '';
this.itemRecentlySelected = false;
this.isExpanded = false;
},
closeResultList() {
this.$refs.selectBase.collapse();
},
setValue(item) {
this.itemRecentlySelected = true;
if (!this.disableAutoClose) {
this.closeResultList();
}
// This is a little against v-model. But so we dont need to load the selected item on every selection
// from the server
this.lastSelection = item;
this.$emit('change', item.id, item);
this.$emit('option-select', Utils.string.camelCase(this.entity), item);
},
clearSelection() {
this.$emit('before-selection-clear', this.singleSelection, this.value);
this.$emit('change', null);
this.$emit('option-select', Utils.string.camelCase(this.entity), null);
},
resetActiveItem(pos = 0) {
// Return if the result list is closed before the search request returns
if (!this.$refs.resultsList) {
return;
}
// If an item is selected the second entry is the first search result
if (this.singleSelection) {
pos = 1;
}
this.$refs.resultsList.setActiveItemIndex(pos);
},
onInputSearchTerm(event) {
const value = event.target.value;
this.$emit('search-term-change', value);
this.debouncedSearch();
},
getKey(object, keyPath, defaultValue) {
return get(object, keyPath, defaultValue);
},
},
});
And here you can find the whole gist containing this index.js, the html.twig file and the scss : https://gist.github.com/Youmar0504/2154bd1d16866d14644aa2a3a6fd513f
ALREADY TRIED
value: {
type: String,
required: false,
default: '',
},
value: {
type: Array,
required: false,
default: [],
},
I would assume you have an extra closing '}' at the value prop.
props: {
value: {
required: false,
} <--
},
highlightSearchTerm: {
type: Boolean,
required: false,
default: true,
},
placeholder: {
type: String,
required: false,
default: '',
},
resetOption: {
type: String,
required: false,
default: '',
},
labelProperty: {
type: [String, Array],
required: false,
default: 'name',
},
labelCallback: {
type: Function,
required: false,
default: null,
},
entity: {
required: true,
type: String,
},
resultLimit: {
type: Number,
required: false,
default: 25,
},
criteria: {
type: Object,
required: false,
default() {
return (new Criteria(1, this.resultLimit)).getAssociation('stateMachine').addFilter(Criteria.equals('state_machine_state.stateMachine.technicalName', 'order_transaction.state'));
},
},
context: {
type: Object,
required: false,
default() {
return Shopware.Context.api;
},
},
disableAutoClose: {
type: Boolean,
required: false,
default: false,
},
},

Shopware 6: How to get all properties in custom module

I have one custom module and I need to get properties list in this module as a multi-select.
How it's possible in shopware 6?
I did try below code, but I can't get it.
custom/plugins/CustomProduct/src/Resources/app/administration/src/module/custom-product/page/custom-product-detail/custom-product-detail.html.twig
{% block custom_product_detail %}
<sw-page class="custom-product-detail">
<template slot="smart-bar-actions">
<sw-button
:disabled="isLoading"
:routerLink="{ name: 'custom.product.list' }">
{{ $t('custom-product.detail.actions.cancel-button') }}
</sw-button>
<sw-button-process
:isLoading="isLoading"
custom="primary"
#process-finish="saveFinish"
#click="onClickSave">
{{ $t('custom-product.detail.actions.save-button') }}
</sw-button-process>
</template>
<template slot="content">
<sw-card-view>
<sw-card
v-if="module"
:isLoading="isLoading"
class="information-card">
<sw-entity-multi-select
:label="$t('custom-product.detail.assignPropertyLabel')"
v-model="module.propertyGroupTranslation">
</sw-entity-multi-select>
</sw-card>
</sw-card-view>
</template>
</sw-page>
{% endblock %}
custom/plugins/CustomProduct/src/Resources/app/administration/src/module/custom-product/page/custom-product-detail/index.js
import template from './custom-product-detail.html.twig';
const { Component, Mixin } = Shopware;
const { Criteria } = Shopware.Data;
const newLocal = 'custom_product';
Component.register('custom-product-detail', {
template,
inject: [
'repositoryFactory',
],
mixins: [
Mixin.getByName('notification')
],
metaInfo() {
return {
title: this.$createTitle()
};
},
data() {
return {
module: null,
isLoading: false,
processSuccess: false,
repository: null
};
},
created() {
this.repository = this.repositoryFactory.create(newLocal);
this.getCustom();
},
computed: {
propertyGroupRepository() {
return this.repositoryFactory.create('property_group');
},
customProperties() {
const criteria = new Criteria();
criteria.addFilter(Criteria.equals('relations.entityName', 'property_group'));
return criteria;
}
},
methods: {
getCustom() {
this.customProperties();
const criteria = new Criteria();
criteria.addAssociation('propertyGroupTranslation');
this.repository
.get(this.$route.params.id, Shopware.Context.api, criteria)
.then((entity) => {
this.module = entity;
});
},
onClickSave() {
this.isLoading = true;
this.repository
.save(this.module, Shopware.Context.api)
.then(() => {
this.getCustom();
this.isLoading = false;
this.processSuccess = true;
}).catch((exception) => {
this.isLoading = false;
this.createNotificationError({
title: this.$t('custom-variant-product.detail.error-message'),
message: exception
});
});
},
saveFinish() {
this.processSuccess = false;
}
}
});
I got the solution.
custom/plugins/CustomProduct/src/Resources/app/administration/src/module/custom-product/page/custom-product-detail/custom-product-detail.html.twig
<sw-entity-multi-select
:label="$t('custom-product.detail.assignPropertyLabel')"
v-model="module.propertyGroupTranslation">
</sw-entity-multi-select>
replace to
<sw-multi-select
v-if="customProduct"
:label="$t('custom-product.detail.assign-property-group-options')"
:options="propertyOpt"
labelProperty="translated.name"
valueProperty="id">
</sw-multi-select>
custom/plugins/CustomProduct/src/Resources/app/administration/src/module/custom-product/page/custom-product-detail/index.js
import template from './custom-product-detail.html.twig';
const { Component, Mixin } = Shopware;
const { Criteria } = Shopware.Data;
Component.register('custom-product-detail', {
template,
inject: [
'repositoryFactory'
],
mixins: [
Mixin.getByName('notification')
],
metaInfo() {
return {
title: this.$createTitle()
};
},
data() {
return {
customProductId: undefined,
customProduct: {},
isLoading: false,
processSuccess: false,
propertyOpt: [],
};
},
created() {
this.createdComponent();
},
computed: {
propertyOptionCriteria() {
const criteria = new Criteria();
criteria.addAssociation('group');
return criteria;
},
customRepository() {
return this.repositoryFactory.create('custom_product');
},
propertyOptionRepository() {
return this.repositoryFactory.create('property_group_option');
},
},
methods: {
getModule() {
this.customRepository.get(this.$route.params.id, Shopware.Context.api).then((entity) => {
this.customProduct = entity;
this.isLoading = false;
});
},
createdComponent() {
this.isLoading = true;
if (this.$route.params.id && this.customProduct.isLoading !== true) {
this.customProductId = this.$route.params.id;
this.loadPropertyOption();
}
this.isLoading = false;
},
loadPropertyOption() {
return this.propertyOptionRepository.search(this.propertyOptionCriteria, Shopware.Context.api)
.then((propertyOption) => {
this.propertyOpt = propertyOption;
});
},
}
});

Getter undefined vuex and axios

I’m trying to get a getter in my component but it says an error. This is my code store.js
const store = new Vuex.Store({
state: {
config:{
themes: [],
typographies:[],
},
user: {
typography_id: 1,
theme_id: null
}
},
mutations: {
FETCH_CONFIG(state, config) {
state.config.themes = config.themes;
state.config.typographies = config.typographies;
},
FETCH_USER(state, user) {
state.user.theme_id = user.theme_id;
state.user.typography_id = user.typography_id;
},
},
actions: {
fetchConfig({commit}) {
axios.get('/api/config').then( function( response ){
commit('FETCH_CONFIG', response.data);
});
},
fetchUser({commit}) {
axios.get('/api/user').then( function( response ){
commit('FETCH_USER', response.data.data[0]);
});
},
},
getters: {
themes(state) {
return state.config.themes;
},
typographies(state) {
return state.config.typographies;
},
typography(state) {
if (state.user.theme_id == 1) {
return state.user.typography_id;
} else {
var theme = state.config.themes.filter(function (el) {
return el.id == state.user.theme_id;
});
return theme[0].typography_id;
}
},
user_theme(state) {
return state.user.theme_id;
},
}
});
And in my component in computed I have:
...mapGetters(['typographies', 'typography'])
And ths is the error I get:
I guess I’m doing something wrong but I don’t know what.
Your getter for typography returns the error because first it goes into the else and then tries to return theme[0].typography_id - but there is an empty array.. if you are loading the date, later on, make sure that the getter returns null before data is loaded.. like:
const store = new Vuex.Store({
state: {
config:{
themes: [],
typographies:[],
},
user: {
typography_id: 1,
theme_id: null
}
},
mutations: {
FETCH_CONFIG(state, config) {
state.config.themes = config.themes;
state.config.typographies = config.typographies;
},
FETCH_USER(state, user) {
state.user.theme_id = user.theme_id;
state.user.typography_id = user.typography_id;
},
},
actions: {
fetchConfig({commit}) {
axios.get('/api/config').then( function( response ){
commit('FETCH_CONFIG', response.data);
});
},
fetchUser({commit}) {
axios.get('/api/user').then( function( response ){
commit('FETCH_USER', response.data.data[0]);
});
},
},
getters: {
themes(state) {
return state.config.themes;
},
typographies(state) {
return state.config.typographies;
},
typography(state) {
if (state.user.theme_id == 1) {
return state.user.typography_id;
} else {
var theme = state.config.themes.filter(function (el) {
return el.id == state.user.theme_id;
});
return theme.length > 0 ? theme[0].typography_id: 1;
}
},
user_theme(state) {
return state.user.theme_id;
},
}
});

Vue - Vuetify server-side datatable bug

I noticed a bug with my vuetify server-side data table. When a data table contains no data and then I attempt to add a new one, it will be successful but it is not displayed in the table, I even checked my database. After I hit the refresh button of the browser, it now shows.
But when a data already exist (even just one), the newly added data automatically reflects in the table. I don't know what is causing this behavior and it only happens when there is no data. I think it has something to do with an empty table being rendered.
Template
<v-data-table
:headers="headers"
:items="tableData"
:editItem="this.editItem"
:deleteItem="this.deleteItem"
:options.sync="pagination"
:server-items-length="totalData"
:loading="loading"
dense
>
Script
export default {
data() {
return {
editedIndex: -1,
search: "",
loading: true,
pagination: {},
dialog: false,
valid: false,
validationErrors: '',
tableData: [],
totalData: 0,
departments: [],
tableItem: {
name: '',
departmend_id: '',
},
snackbar: {
enable: false,
name: '',
message: '',
},
headers: [
{ text: 'ID', value: 'id' },
{ text: 'Department Name', value: 'name' },
{ text: 'From Department', value: 'department' },
{ text: 'Created At', value: 'created_at' },
{ text: 'Action', value: 'action' },
],
rules: {
name: [
v => !!v || 'This field is required',
v => (v && v.length <= 50) || 'Field must be less than 50 characters'
]
}
}
},
watch: {
params: {
handler() {
this.getDataFromApi().then(data => {
this.tableData = data.items
this.totalData = data.total
})
},
deep: true
}
},
computed: {
params(nv) {
return {
...this.pagination,
query: this.search
}
},
formTitle() {
return this.editedIndex === -1 ? 'New Section' : this.tableItem.name
},
},
mounted() {
this.getDataFromApi().then(data => {
this.tableData = data.items
this.totalData = data.total
})
},
created() {
this.getDepartments()
},
methods: {
async getDataFromApi() {
this.loading = true
return new Promise((resolve, reject) => {
const { sortBy, sortDesc, page, itemsPerPage } = this.pagination
let search = this.search.trim().toLowerCase()
axios.get('/api/sections').then(res => {
let items = _.orderBy(res.data, ['created_at'], ['desc'])
const total = items.length
if (search) {
items = items.filter(item => {
return Object.values(item).join(",").toLowerCase().includes(search)
})
}
if (sortBy.length === 1 && sortDesc.length === 1) {
items = items.sort((a, b) => {
const sortA = a[sortBy[0]]
const sortB = b[sortBy[0]]
if (sortDesc[0]) {
if (sortA < sortB) return 1
if (sortA > sortB) return -1
return 0
} else {
if (sortA < sortB) return -1
if (sortA > sortB) return 1
return 0
}
})
}
if (itemsPerPage > 0) {
items = items.slice((page - 1) * itemsPerPage, page * itemsPerPage)
}
setTimeout(() => {
this.loading = false
resolve({
items,
total
})
}, 300)
})
})
},
async initialize() {
this.loading = true
let res = await axios.get('/api/sections')
this.tableData = _.orderBy(res.data, ['created_at'], ['desc'])
setTimeout(() => {
this.loading = false
}, 300)
},
async getDepartments() {
let res = await axios.get('/api/departments')
this.departments = _.orderBy(res.data, ['name'], ['asc'])
},
reset() {
this.$refs.form.reset()
},
close() {
this.dialog = false
this.$refs.form.reset()
setTimeout(() => {
this.editedIndex = -1
}, 300)
},
editItem(item) {
this.editedIndex = this.tableData.indexOf(item)
this.tableItem = Object.assign({}, item)
this.dialog = true
},
updateSnackbar(e) {
this.snackbar.enable = e
},
async deleteItem(item) {
let index = this.tableData.indexOf(item)
if(confirm('Are you sure you want to delete this item?') && this.tableData.splice(index, 1)) {
let res = await axios.delete('/api/sections/' + item.id)
this.snackbar.name = ''
this.snackbar.message = res.data.message
this.snackbar.enable = true
}
},
async save() {
if (this.editedIndex > -1) {
try {
Object.assign(this.tableData[this.editedIndex], this.tableItem)
let res = await axios.put('/api/sections/' + this.tableItem.id, this.tableItem)
this.snackbar.name = res.data.name
this.snackbar.message = res.data.message
} catch(error) {
if (error.response.status == 422){
this.validationErrors = error.response.data.errors
}
}
} else {
try {
let res = await axios.post('/api/sections', this.tableItem)
this.snackbar.name = res.data.name
this.snackbar.message = res.data.message
} catch(error) {
if (error.response.status == 422){
this.validationErrors = error.response.data.errors
}
}
}
await this.initialize()
this.snackbar.enable = true
this.close()
this.reset()
},
}
}
So after countless of trials, I have finally found the solution.
try {
let res = await axios.post('/api/departments', this.tableItem)
this.getDataFromApi().then(data => {
this.tableData = data.items
this.totalData = data.total
})
}