Skip to content

Instantly share code, notes, and snippets.

@x8BitRain
Created September 6, 2023 09:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save x8BitRain/3ec55df633c6277ceb8f51c5c3523743 to your computer and use it in GitHub Desktop.
Save x8BitRain/3ec55df633c6277ceb8f51c5c3523743 to your computer and use it in GitHub Desktop.
ember table example
<NwKeywordTable::Provider
@scope={{@scope}}
@url={{@url}}
@dynamicView={{@dynamicView}}
@sort={{@sort}}
@direction={{@direction}}
@onSort={{this.sortKeywords}}
@keywords={{this.keywords.data}}
@fetchKeywordStats={{this.fetchKeywordStats}}
@filterChange={{this.filterChange}}
as |provider|
>
<NwTable
@provider={{provider}}
@tableData={{this.keywords.data}}
@isLoading={{or
this.keywords.loading
provider.deleteSelectedKeywords.isRunning
provider.deleteKeyword.isRunning
}}
@estimateHeight={{58}}
@isDraggable={{true}}
@containerSelector=".main-area-wrapper"
{{evolution-tooltips}}
>
<:row as |keyword|>
<NwKeywordTable::Row
@keyword={{keyword}}
@selectedKeywords={{provider.selectedKeywords}}
@deleteKeyword={{provider.deleteKeyword}}
@engineTitle={{provider.engineTitle}}
@toggleDataSelected={{provider.toggleKeywordSelected}}
@selectedColumns={{provider.columns.selected}}
@keywordsDeletable={{provider.keywordsDeletable}}
/>
</:row>
<:empty>
<h4>
No keywords to show.
</h4>
</:empty>
</NwTable>
</NwKeywordTable::Provider>
<NwKeywordsPage::Pagination
@currentPage={{@page}}
@pages={{this.keywords.data.meta.pages}}
@onPageUpdated={{this.pageUpdated}}
/>
<!-- this has no controller -->
{{#let (and (not @isLoading) (eq @tableData.length 0)) as |empty|}}
<div
class="nw-table
{{if @isLoading 'nw-table--loading'}}
{{if empty 'nw-table--empty'}}"
...attributes
>
<NwTable::Header
@provider={{@provider}}
@isDraggable={{@isDraggable}}
@hideCheckbox={{@hideCheckbox}}
/>
{{#if empty}}
<div class="nw-table__empty">
{{yield to="empty"}}
</div>
{{else}}
{{#if @tableData}}
<VerticalCollection
@items={{@tableData}}
@estimateHeight={{@estimateHeight}}
@containerSelector={{@containerSelector}}
@bufferSize={{4}}
@staticHeight={{or @staticHeight true}}
as |data|
>
{{yield data to="row"}}
</VerticalCollection>
{{else}}
<div class="nw-table-row nw-table-row--loading">
<Loading::SmallHorizontal />
</div>
{{/if}}
{{/if}}
</div>
{{/let}}
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
/**
@argument currentPage
@argument pages
@argument limit
@argument onLimitUpdated
@argument onPageUpdated
*/
export default class NwKeywordPagePaginationComponent extends Component {
@service userSettings;
limits = [50, 100, 250, 500];
defaultLimit = 50;
get pagesList() {
const pagesCount = Number.parseInt(this.args.pages, 10);
let pages =
pagesCount && Number.parseInt(pagesCount, 10) > 0
? [...Array(pagesCount).keys()].map((p) => ++p)
: [];
if (pages.length > 10) {
// show first, four pages before current, four pages after current and last
const currentPage = this.args.currentPage;
const left = [...Array(4).keys()]
.map((n) => currentPage - (4 - n))
.filter((p) => p > 1);
const right = [...Array(4).keys()]
.map((n) => currentPage + (1 + n))
.filter((p) => p < pagesCount);
pages = [1]
.concat(left)
.concat([currentPage])
.concat(right)
.concat([pagesCount])
.uniq();
const pagesWithTruncation = [];
for (let index = 0; index < pages.length; index++) {
const page = pages[index];
pagesWithTruncation.push(page);
if (pages[index + 1] && pages[index + 1] - page !== 1) {
pagesWithTruncation.push(null); //ellipsis in template
}
}
pages = pagesWithTruncation;
}
return pages;
}
get paginationShown() {
return this.pagesList.length > 1 || this.args.limit > this.defaultLimit;
}
get previousPage() {
const previous = Number.parseInt(this.args.currentPage, 10) - 1;
return this.pagesList.includes(previous) ? previous : null;
}
get nextPage() {
const next = Number.parseInt(this.args.currentPage, 10) + 1;
return this.pagesList.includes(next) ? next : null;
}
}
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { allSearchEngines } from 'nightwatch-web/constants/keyword-data';
import isEmpty from 'lodash-es/isEmpty';
import xor from 'lodash-es/xor';
import underscorifyKeys from 'nightwatch-web/utils/underscorify-keys';
import { task } from 'ember-concurrency';
import isTesting from '../../utils/is-testing';
import KeywordTableColumns from '../../utils/tables/keyword-table-columns';
import { getOwner } from '@ember/application';
/**
@argument scope - one of url, dynamicView, newDynamicView, urlDynamicView, newUrlDynamicView
@argument url
@argument dynamicView
@argument sort
@argument direction
@argument onSort
@argument keywords
*/
export default class NwKeywordTableProviderComponent extends Component {
@service userSettings;
@service metrics;
@service router;
@service filterGroupHandling;
@service keywordColumnsData;
@service('persistence/keyword-list-sorting') keywordListSorting;
@service session;
@service fetch;
@service store;
@tracked selectedKeywordIds = [];
previousResourceId = null;
columns = new KeywordTableColumns({
owner: getOwner(this),
saveColumnNames: this.saveColumnNames,
});
@action
initColumns() {
if (
this.columns.selected.length === 0 ||
this.resource?.id !== this.previousResourceId
) {
this.columns.init(this.loadColumnNames, this.args.url?.competitors);
}
this.previousResourceId = this.resource?.id;
}
get engineTitle() {
return allSearchEngines.reduce((acc, { id, text }) => {
acc[id] = text;
return acc;
}, {});
}
get resource() {
const { scope, url, dynamicView } = this.args;
if (scope === 'url') {
return url;
} else if (scope.toLowerCase().includes('view')) {
return dynamicView;
} else {
throw `NwKeywordTableProviderComponent: unknown scope (${scope})`;
}
}
get isNewView() {
return ['newDynamicView', 'newUrlDynamicView'].includes(this.args.scope);
}
get keywordsDeletable() {
return this.args.scope === 'url';
}
get isLoaded() {
return this.args.keywords?.isLoaded || this.args.keywords?.isFulfilled;
}
get oppositeDirection() {
return this.args.direction === 'asc' ? 'desc' : 'asc';
}
get selectedKeywords() {
if (!this.args.keywords) return [];
return this.args.keywords.filter((keyword) =>
this.selectedKeywordIds.includes(keyword.id)
);
}
get allKeywordsSelected() {
const { keywords, selectedKeywords } = this.args;
if (!keywords || keywords?.length === 0) return false;
return isEmpty(xor(selectedKeywords?.mapBy('id'), keywords?.mapBy('id')));
}
@action
toggleKeywordSelected(keyword) {
if (this.selectedKeywordIds.includes(keyword.id)) {
this.selectedKeywordIds.removeObject(keyword.id);
} else {
this.selectedKeywordIds.pushObject(keyword.id);
}
}
@action
selectAll() {
this.selectedKeywordIds = this.args.keywords.mapBy('id');
}
@action
selectNone() {
this.selectedKeywordIds = [];
}
@action
onSelectAllToggleChange(checked) {
checked ? this.selectAll() : this.selectNone();
}
@action
onSort(field, direction) {
if (!field) return;
this.args.onSort(field, direction);
this.saveSortParams(field, direction);
this.metrics.trackEvent({ event: 'Sorted Keywords' });
}
saveSortParams(sortField, sortDirection) {
const { resource } = this;
const { scope } = this.args;
if (!resource) return;
const params = { sortField, sortDirection };
if (scope.toLowerCase().includes('view')) {
params['viewId'] = resource.id;
} else if (scope.toLowerCase().includes('url')) {
params['urlId'] = resource.id;
}
this.keywordListSorting.persistSorting(params);
}
@action
openRankingUrl(url) {
return window.open(url);
}
@task({ drop: true })
*deleteKeyword(keyword) {
if (!isTesting && !confirm('Are you sure you want to delete this keyword?'))
return;
yield keyword.destroyRecord();
this.afterKeywordsDeleted([keyword]);
}
@task({ drop: true })
*deleteSelectedKeywords() {
const keywords = this.selectedKeywords.slice();
if (
!isTesting &&
!confirm(
`Are you sure you want to delete ${keywords.length} ${
keywords.length === 1 ? 'keyword' : 'keywords'
}? All associated data will be lost.`
)
)
return;
yield this.fetch.post(`/urls/${this.args.url.id}/keywords/batch_destroy`, {
data: underscorifyKeys({ keywordIds: keywords.mapBy('id') }),
});
keywords.forEach((keyword) => this.store.unloadRecord(keyword));
this.afterKeywordsDeleted(keywords);
}
get loadColumnNames() {
const searchKeywordUrlId = this.args.url?.id;
const dynamicViewId = this.args.dynamicView?.id;
return this.userSettings.getSetting({
searchKeywordUrlId,
dynamicViewId,
})?.settingsData?.keywords?.columns;
}
@action
saveColumnNames(columnNames) {
this.metrics.trackEvent({
event: 'Switched Columns',
table: 'Keywords',
columns: columnNames.join(', '),
});
if (this.isNewView) return;
const searchKeywordUrlId = this.args.url?.id;
const dynamicViewId = this.args.dynamicView?.id;
return this.userSettings.saveSetting({
searchKeywordUrlId,
dynamicViewId,
settingsData: {
keywords: {
columns: columnNames,
},
},
});
}
afterKeywordsDeleted(keywords) {
const meta = this.args.keywords.meta;
meta.total = meta.total - keywords.length;
this.session.user.reload(); // Refresh account uasge
this.args.url.reload(); // Refresh url usage
this.metrics.trackEvent({
event: 'Removed Keywords',
keywords: keywords.mapBy('query').join(', '),
});
this.args.fetchKeywordStats();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment