Related
How to get all posts by a category slug in GROQ?
You can see that a post is added to one or more categories. I would like to get all posts by a category slug to show the posts on a category page. I am new to Sanity.io's GROQ. All the tutorials I have found on creating a blog with sanity.io and next.js have not covered it to show a category page showing posts from a category.
Post schema:
export default {
name: 'post',
title: 'Post',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
},
{
name: 'author',
title: 'Author',
type: 'reference',
to: {type: 'author'},
},
{
name: 'mainImage',
title: 'Main image',
type: 'image',
options: {
hotspot: true,
},
},
{
name: 'categories',
title: 'Categories',
type: 'array',
of: [{type: 'reference', to: {type: 'category'}}],
},
{
name: 'publishedAt',
title: 'Published at',
type: 'datetime',
},
{
name: 'body',
title: 'Body',
type: 'blockContent',
},
],
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage',
},
prepare(selection) {
const {author} = selection
return Object.assign({}, selection, {
subtitle: author && `by ${author}`,
})
},
},
}
I have tried the following:
const query = `*[_type == 'post' && $slug in [categories[]-> {slug}] ]{ _id, title, slug, publishedAt, categories[]-> {title,slug}} | order(publishedAt) [0...10]`;
const posts = await client.fetch(
query,
{ slug }
);
console.log("posts", posts);
I got the solution at Sanity's slack channel. The following code helps to get the posts from a category by it's slug.
`*[_type == 'post' && $slug in categories[]->slug.current ]`;
I found a TreeGrid extension for DataTables:
https://homfen.github.io/dataTables.treeGrid.js/
but instead of the name I would like to add a column between name and position and place a checkbox here.
However when I do this e.g.:
var columns = [
{
title: '',
target: 0,
className: 'treegrid-control',
data: function (item) {
if (item.children) {
return '<span>+<\/span>';
}
return '';
}
},
{
title: 'Name',
target: 1,
data: function (item) {
return item.name;
}
},
{
defaultContent: '',
target: 2,
className: 'select-checkbox',
function(item) {
return item;
}
},
{
title: 'Position',
target: 3,
data: function (item) {
return item.position;
}
},
{
title: 'Office',
target: 4,
data: function (item) {
return item.office;
}
},
{
title: 'Extn.',
target: 5,
data: function (item) {
return item.extn;
}
},
{
title: 'Start date',
target: 6,
data: function (item) {
return item.start;
}
},
{
title: 'Salary',
target:7,
data: function (item) {
return item.salary;
}
}
];
I get an extra column but when checking the parent does not select all underlying children rows.
Anyone have an idea how to establish this?
Edit: updated the columns definition.
When I add a button to read the selected values e.g.:
dom: 'Bfrtip',
select:true,
buttons: [
{
text: 'Alert selected',
action: function(e, dt, node, config) {
var data = table.rows({
selected: true
}).data().toArray();
var i;
var text = new Array();
for (i = 0; i < data.length; i++) {
text.push(data[i].name);
}
alert("you selected: " + text.join(",") );
console.log("text---" + text.join(","));
}
}
]
the table starts to behave oddly for example: the selection of underlying children stops.
Does anyone know an easy way to get a percent done by story hours related to 'Portfolioitem/Saga'? The existing portfolio items app in CA only shows percent done by story plan estimate and I'd like to get the task hours details as well.
I've written the following custom app to include the data but my table gets loaded before the data is returned. Please help!
Ext.define('Rally.app.PortfolioGrid', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
console.log('Working');
this._loadComboBox();
},
_loadComboBox : function() {
this.searchComboBox = Ext.create('Rally.ui.combobox.ArtifactSearchComboBox', {
fieldLabel: 'Saga',
noEntryText: '',
emptyText: 'Enter saga ID or keyword...',
//grow: true,
//allowNoEntry: false,
storeConfig: {
autoLoad: true,
models: ['PortfolioItem/Saga']
},
listeners: {
select: function(combobox, records) {
this._loadData();
},
scope: this
}
});
this.add(this.searchComboBox);
},
_loadData: function () {
var selectedID = this.searchComboBox.getRecord().get('FormattedID');
console.log('Selected Saga', selectedID);
var myFilters = [
{
property: 'FormattedID',
operation: '=',
value: selectedID
},
];
if (this.myStore) {
console.log('store exists');
this.myStore.setFilter(myFilters);
this.myStore.load();
// create store
} else {
this.myStore = Ext.create('Rally.data.wsapi.Store', {
model: 'PortfolioItem/Saga',
autoLoad: true,
fetch: ['FormattedID', 'Name', 'Children', 'Release', 'State', 'PercentDoneByStoryPlanEstimate', 'PercentDoneByStoryCount', 'Project', 'Owner'],
filters: myFilters,
listeners: {
load: this._onDataLoaded,
scope: this
}
});
}
},
_onDataLoaded: function(store, data){
var features = [];
var pendingstories = data.length;
Ext.Array.each(data, function(feature) {
var f = {
FormattedID: feature.get('FormattedID'),
Name: feature.get('Name'),
_ref: feature.get("_ref"),
State: (feature.get('State') && feature.get('State').Name) || ' ',
Project: (feature.get('Project') && feature.get('Project').Name) || ' ',
Owner: (feature.get('Owner') && feature.get('Owner')._refObjectName) || 'No Owner',
PercentDoneByStoryPlanEstimate: Math.floor(feature.get('PercentDoneByStoryPlanEstimate') * 100) + '%',
PercentDoneByStoryCount: Math.floor(feature.get('PercentDoneByStoryCount') * 100) + '%',
Children: [],
totalPlannedHours: 0,
totalHoursRemaining: 0,
totalHoursCompleted: 0,
Percentage: 0
};
var sagaFeatures = feature.getCollection('Children');
sagaFeatures.load({
fetch: ['FormattedID', 'Parent', 'UserStories', 'DirectChildrenCount'],
callback: function(records, operation, success){
Ext.Array.each(records, function(child){
var s = {
FormattedID: child.get('FormattedID'),
};
if (child.get('DirectChildrenCount') > 0) {
child.getCollection('UserStories').load({
fetch: ['FormattedID', 'TaskEstimateTotal', 'TaskRemainingTotal'],
callback: function(records, operation, success) {
Ext.Array.each(records, function(us) {
f.totalPlannedHours += us.get('TaskEstimateTotal');
f.totalHoursRemaining += us.get('TaskRemainingTotal');
f.totalHoursCompleted = f.totalPlannedHours - f.totalHoursRemaining
console.log("Total Hours Completed", f.totalHoursRemaining)
f.Percentage = Math.floor((f.totalHoursCompleted / f.totalPlannedHours) * 100) + '%';
}, this);
}, scope: this});
}
f.Children.push(s);
}, this);
},
scope: this
});
features.push(f);
--pendingstories;
if (pendingstories === 0) {
this._createGrid(features);
}
}, this);
this._createGrid(features);
},
_createGrid: function(features) {
var myCustomStore = Ext.create('Rally.data.custom.Store', {
data: features
//pageSize: 100,
});
if (!this.grid) {
this.grid = this.add({
xtype: 'rallygrid',
flex: 1,
store: myCustomStore,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'State', dataIndex: 'State'
},
{
text: 'Saga Features', dataIndex: 'Children',
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(child){
html.push('' + child.FormattedID + '')
});
return html.join(', ');
}
},
{
text: 'Percent Done By Story Plan Estimate', dataIndex: 'PercentDoneByStoryPlanEstimate'
},
{
text: 'Percent Done By Story Count', dataIndex: 'PercentDoneByStoryCount'
},
{
text: 'Total Planned Hours', dataIndex: 'totalPlannedHours'
},
{
text: 'Total Completed Hours', dataIndex: 'totalHoursCompleted'
},
{
text: 'Percent Done by Story Hours', dataIndex: 'Percentage'
},
{
text: 'Project', dataIndex: 'Project'
},
{
text: 'Owner', dataIndex: 'Owner'
}
]
});
}
else {
this.grid.reconfigure(myCustomStore);
}
}
});
Can you load the data from the bottom up instead and save yourself all the many nested loops and store loads?
I think you should be able to just create a store of stories beneath the selected saga...
this.myStore = Ext.create('Rally.data.wsapi.Store', {
model: 'UserStory',
autoLoad: true,
limit: Infinity,
pageSize: 2000,
fetch: ['FormattedID', 'Name', 'TaskEstimateTotal', 'TaskRemainingTotal', 'Feature', 'Parent', 'Children'],
filters: [{ property: 'Feature.Parent', operator: '=', value: this.searchComboBox.getRecord().get('_ref')}],
listeners: {
load: this._onDataLoaded,
scope: this
}
});
And then just crunch your data into the appropriate data structure, bucketing them by Feature. Something like this?
_onDataLoaded: function(store) {
var sagas = {};
var stories = store.getRange();
_.each(stories, function(story) {
var feature = store.get('Feature'),
saga = feature.Parent;
if (!sagas[saga._ref]) {
sagas[saga._ref] = Ext.clone(saga);
}
var featureChildren = sagas[saga._ref].Children.values = sagas[saga._ref].Children.values || {};
if (!featureChildren[feature._ref]) {
featureChildren[feature._ref] = Ext.clone(feature);
}
var storyChildren = featureChildren[feature._ref].UserStories.values = featureChildren[feature._ref].UserStories.values || {};
if (!storyChildren[story._ref]) {
storyChildren[story._ref] = Ext.clone(story);
}
});
//now you have an object of sagas, just needs to be an array for your custom store data
var customStoreData = _.values(sagas);
this._createGrid(customStoreData);
}
Something like that should get you close. You'll need to move around your code a little bit where you're calculating your rollup totals, and your renderer for the Children column will be a tiny bit different- just loop over _.values(value.values) instead.
I want to show some data from Rally using snapshot sotre passed to teh chart like this:
storeConfig: {
find: {
_ItemHierarchy: 15312401235, //PI Object ID
//Release: 9045474054,
_TypeHierarchy: 'HierarchicalRequirement', //Burn on stories
Children: null, //Only include leaf stories,
_ValidTo: { $gte: me._startDateField.value },
_ValidFrom: { $lte: me._endDateField.value }
},
fetch: ['ScheduleState', 'PlanEstimate'],
hydrate: ['ScheduleState'],
sort: {
'_ValidFrom': 1
}
}
The idea is that I want the chart to show only yhe period between Start Date and End Date specified in me._startDateField.value and me._endDateField.value. What is the way of achieving this? Because now the chart displays the data starting from January and not from Start Date.
This example restricts the end date to a selection in the second rallydatepicker instead of defaulting to today's date. See Readme here.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
var that = this;
var minDate = new Date(new Date() - 86400000*90); //milliseconds in day = 86400000
var datePicker = Ext.create('Ext.panel.Panel', {
title: 'Choose start and end dates:',
bodyPadding: 10,
renderTo: Ext.getBody(),
layout: 'hbox',
items: [{
xtype: 'rallydatepicker',
itemId: 'from',
minDate: minDate,
handler: function(picker, date) {
that.onStartDateSelected(date);
}
},
{
xtype: 'rallydatepicker',
itemId: 'to',
minDate: minDate,
handler: function(picker, date) {
that.onEndDateSelected(date);
}
}]
});
this.add(datePicker);
var panel = Ext.create('Ext.panel.Panel', {
id:'infoPanel',
componentCls: 'panel'
});
this.add(panel);
},
onStartDateSelected:function(date){
console.log(date);
this._startDate = date;
},
onEndDateSelected:function(date){
this._endDate = date;
console.log(date);
Ext.getCmp('infoPanel').update('showing data between ' + this._startDate + ' and ' + this._endDate);
this.defineCalculator();
this.makeChart();
},
defineCalculator: function(){
var that = this;
Ext.define("MyDefectCalculator", {
extend: "Rally.data.lookback.calculator.TimeSeriesCalculator",
getMetrics: function () {
var metrics = [
{
field: "State",
as: "Open",
display: "column",
f: "filteredCount",
filterField: "State",
filterValues: ["Submitted","Open"]
},
{
field: "State",
as: "Closed",
display: "column",
f: "filteredCount",
filterField: "State",
filterValues: ["Fixed","Closed"]
}
];
return metrics;
}
});
},
makeChart: function(){
if (this.down('#myChart')) {
this.remove('myChart');
}
var timePeriod = new Date(this._endDate - this._startDate);
var project = this.getContext().getProject().ObjectID;
var storeConfig = this.createStoreConfig(project, timePeriod);
this.chartConfig.calculatorConfig.startDate = Rally.util.DateTime.format(new Date(this._startDate), 'Y-m-d');
this.chartConfig.calculatorConfig.endDate = Rally.util.DateTime.format(new Date(this._endDate), 'Y-m-d');
this.chartConfig.storeConfig = storeConfig;
this.add(this.chartConfig);
},
createStoreConfig : function(project, interval ) {
return {
listeners : {
load : function(store,data) {
console.log("data",data.length);
}
},
filters: [
{
property: '_ProjectHierarchy',
operator : 'in',
value : [project]
},
{
property: '_TypeHierarchy',
operator: 'in',
value: ['Defect']
},
{
property: '_ValidFrom',
operator: '>=',
value: interval
}
],
autoLoad : true,
limit: Infinity,
fetch: ['State'],
hydrate: ['State']
};
},
chartConfig: {
xtype: 'rallychart',
itemId : 'myChart',
chartColors: ['Red', 'Green'],
storeConfig: { },
calculatorType: 'MyDefectCalculator',
calculatorConfig: {
},
chartConfig: {
plotOptions: {
column: { stacking: 'normal'}
},
chart: { },
title: { text: 'Open/Closed Defects'},
xAxis: {
tickInterval: 1,
labels: {
formatter: function() {
var d = new Date(this.value);
return ""+(d.getMonth()+1)+"/"+d.getDate();
}
},
title: {
text: 'Date'
}
},
yAxis: [
{
title: {
text: 'Count'
}
}
]
}
}
});
I have a custom grid that displays open tasks filtered by (Owner = some-user#company.com).
I would like to include the last revision for each task in a custom grid, but the Revision column is not available on the settings dialog. How to traverse from Revision History to individual revisions?
It can't be done with a custom grid, but can be done with a custom code. Here is an app example that populates a grid based on a selection in the UserSearchComboBox , and then displays the last revision of a selected task on a click event.
You may copy the html file into a custom page from this github repo:
Here is the js file:
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
var context = this.getContext ();
var currentProject = context.getProject()._ref;
var panel = Ext.create('Ext.panel.Panel', {
layout: 'hbox',
itemId: 'parentPanel',
componentCls: 'panel',
items: [
{
xtype: 'rallyusersearchcombobox',
fieldLabel: 'SELECT USER:',
project: currentProject,
listeners:{
ready: function(combobox){
this._onUserSelected(combobox.getRecord());
},
select: function(combobox){
if (this.down('#c').html !== 'No task is selected') {
Ext.getCmp('c').update('No task is selected');
}
this._onUserSelected(combobox.getRecord());
},
scope: this
}
},
{
xtype: 'panel',
title: 'Tasks',
width: 600,
itemId: 'childPanel1'
},
{
xtype: 'panel',
title: 'Last Revision',
width: 600,
itemId: 'childPanel2'
}
],
});
this.add(panel);
this.down('#childPanel2').add({
id: 'c',
padding: 10,
maxWidth: 600,
maxHeight: 400,
overflowX: 'auto',
overflowY: 'auto',
html: 'No task is selected'
});
},
_onUserSelected:function(record){
var user = record.data['_ref'];
if(user){
var filter = Ext.create('Rally.data.QueryFilter', {
property: 'Owner',
operator: '=',
value: user
});
filter = filter.and({
property: 'State',
operator: '<',
value: 'Completed'
});
filter.toString();
Ext.create('Rally.data.WsapiDataStore', {
model: 'Task',
fetch: [ 'DragAndDropRank','FormattedID','Name','State','RevisionHistory'],
autoLoad: true,
filters : [filter],
sorters:[
{
property: 'DragAndDropRank',
direction: 'ASC'
}
],
listeners: {
load: this._onTaskDataLoaded,
scope: this
}
});
}
},
_onTaskDataLoaded: function(store, data) {
var customRecords = [];
Ext.Array.each(data, function(task, index) {
customRecords.push({
_ref: task.get('_ref'),
FormattedID: task.get('FormattedID'),
Name: task.get('Name'),
RevisionID: Rally.util.Ref.getOidFromRef(task.get('RevisionHistory')),
});
}, this);
this._updateGrid(store,data);
},
_updateGrid: function(store, data){
if (!this.down('#g')) {
this._createGrid(store,data);
}
else{
this.down('#g').reconfigure(store);
}
},
_createGrid: function(store,data){
var that = this;
var g = Ext.create('Rally.ui.grid.Grid', {
id: 'g',
store: store,
enableRanking: true,
columnCfgs: [
{text: 'Formatted ID', dataIndex: 'FormattedID'},
{text: 'Name', dataIndex: 'Name'},
{text: 'State', dataIndex: 'State'},
{text: 'Last Revision',
renderer: function (v, m, r) {
var id = Ext.id();
Ext.defer(function () {
Ext.widget('button', {
renderTo: id,
text: 'see',
width: 50,
handler: function () {
console.log('r', r.data);
that._getRevisionHistory(data, r.data);
}
});
}, 50);
return Ext.String.format('<div id="{0}"></div>', id);
}
}
],
height: 400,
});
this.down('#childPanel1').add(g);
},
_getRevisionHistory: function(taskList, task) {
this._task = task;
this._revisionModel = Rally.data.ModelFactory.getModel({
type: 'RevisionHistory',
scope: this,
success: this._onModelCreated
});
},
_onModelCreated: function(model) {
model.load(Rally.util.Ref.getOidFromRef(this._task.RevisionHistory._ref),{
scope: this,
success: this._onModelLoaded
});
},
_onModelLoaded: function(record, operation) {
record.getCollection('Revisions').load({
fetch: true,
scope: this,
callback: function(revisions, operation, success) {
this._onRevisionsLoaded(revisions, record);
}
});
},
_onRevisionsLoaded: function(revisions, record) {
var lastRev = _.first(revisions).data;
console.log('_onRevisionsLoaded: ',lastRev.Description, lastRev.RevisionNumber, lastRev.CreationDate );
this._displayLastRevision(lastRev.Description,lastRev.RevisionNumber, lastRev.CreationDate );
},
_displayLastRevision:function(desc, num, date){
Ext.getCmp('c').update('<b>' + this._task.FormattedID + '</b><br/><b>Revision CreationDate: </b>' + date +'<br /><b>Description:</b>' + desc + '<br /><b>Revision Number:</b>' + num + '<br />');
}
});