transaction refactor / cleanup / improvement
This commit is contained in:
parent
f68136bd79
commit
65f65f1aea
16 changed files with 257 additions and 259 deletions
|
@ -8,15 +8,15 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* Added a tipping button to send LBRY Credits to the publisher
|
||||
* Added edit button on published content / improved UX for editing claims.
|
||||
* Added a tipping button to send LBRY Credits to a creator.
|
||||
* Added an edit button on published content. Significantly improved UX for editing claims.
|
||||
* Significantly more detail is shown about past transactions and new filtering options for transactions.
|
||||
* File pages now show the time of a publish.
|
||||
* The "auth token" displayable on Help offers security warning
|
||||
* Added a new component for rendering dates and times. This component can render the date and time of a block height, as well.
|
||||
|
||||
### Changed
|
||||
*
|
||||
*
|
||||
|
||||
### Fixed
|
||||
* URLs on cards no longer wrap and show an ellipsis if longer than one line
|
||||
|
|
|
@ -70,7 +70,7 @@ export class CreditAmount extends React.PureComponent {
|
|||
showFree: React.PropTypes.bool,
|
||||
showFullPrice: React.PropTypes.bool,
|
||||
showPlus: React.PropTypes.bool,
|
||||
look: React.PropTypes.oneOf(["indicator", "plain"]),
|
||||
look: React.PropTypes.oneOf(["indicator", "plain", "fee"]),
|
||||
fee: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
|
@ -79,7 +79,6 @@ export class CreditAmount extends React.PureComponent {
|
|||
label: true,
|
||||
showFree: false,
|
||||
look: "indicator",
|
||||
showFree: false,
|
||||
showFullPrice: false,
|
||||
showPlus: false,
|
||||
fee: false,
|
||||
|
@ -119,10 +118,7 @@ export class CreditAmount extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<span
|
||||
className={`credit-amount credit-amount--${this.props.look} ${this.props
|
||||
.fee
|
||||
? " meta"
|
||||
: ""}`}
|
||||
className={`credit-amount credit-amount--${this.props.look}`}
|
||||
title={fullPrice}
|
||||
>
|
||||
<span>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import React from "react";
|
||||
|
||||
class DateTime extends React.PureComponent {
|
||||
static SHOW_DATE = "date";
|
||||
static SHOW_TIME = "time";
|
||||
static SHOW_BOTH = "both";
|
||||
|
||||
componentWillMount() {
|
||||
this.refreshDate(this.props);
|
||||
}
|
||||
|
@ -17,9 +21,20 @@ class DateTime extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { date } = this.props;
|
||||
const { date, formatOptions } = this.props;
|
||||
const show = this.props.show || DateTime.SHOW_BOTH;
|
||||
|
||||
return <span>{date && date.toLocaleString()}</span>;
|
||||
return (
|
||||
<span>
|
||||
{date &&
|
||||
(show == DateTime.SHOW_BOTH || show === DateTime.SHOW_DATE) &&
|
||||
date.toLocaleDateString()}
|
||||
{show == DateTime.SHOW_BOTH && " "}
|
||||
{date &&
|
||||
(show == DateTime.SHOW_BOTH || show === DateTime.SHOW_TIME) &&
|
||||
date.toLocaleTimeString()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import Link from "./view";
|
||||
import LinkTransaction from "./view";
|
||||
|
||||
export default connect(null, null)(Link);
|
||||
export default connect(null, null)(LinkTransaction);
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { doNavigate } from "actions/navigation";
|
||||
import { selectClaimedRewardsByTransactionId } from "selectors/rewards";
|
||||
import TransactionList from "./view";
|
||||
|
||||
const select = state => ({
|
||||
rewards: selectClaimedRewardsByTransactionId(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||
});
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
import React from "react";
|
||||
import LinkTransaction from "component/linkTransaction";
|
||||
import { CreditAmount } from "component/common";
|
||||
|
||||
class TransactionTableBody extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
getClaimLink(claim_name, claim_id) {
|
||||
let uri = `lbry://${claim_name}#${claim_id}`;
|
||||
|
||||
return (
|
||||
<a className="button-text" onClick={() => this.props.navigate(uri)}>
|
||||
{claim_name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
filterList(transaction) {
|
||||
if (this.props.filter == "claim") {
|
||||
return transaction.claim_info.length > 0;
|
||||
} else if (this.props.filter == "support") {
|
||||
return transaction.support_info.length > 0;
|
||||
} else if (this.props.filter == "update") {
|
||||
return transaction.update_info.length > 0;
|
||||
} else {
|
||||
return transaction;
|
||||
}
|
||||
}
|
||||
|
||||
renderBody(transaction) {
|
||||
const txid = transaction.id;
|
||||
const date = transaction.date;
|
||||
const fee = transaction.fee;
|
||||
const filter = this.props.filter;
|
||||
const options = {
|
||||
weekday: "short",
|
||||
year: "2-digit",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
};
|
||||
|
||||
if (filter == "tipSupport")
|
||||
transaction["tipSupport_info"] = transaction["support_info"].filter(
|
||||
tx => tx.is_tip
|
||||
);
|
||||
|
||||
return filter != "unfiltered"
|
||||
? transaction[`${filter}_info`].map(item => {
|
||||
return (
|
||||
<tr key={`${txid}:${item.nout}`}>
|
||||
<td>
|
||||
{date
|
||||
? date.toLocaleDateString("en-US", options)
|
||||
: <span className="empty">
|
||||
{__("(Transaction pending)")}
|
||||
</span>}
|
||||
</td>
|
||||
<td>
|
||||
<CreditAmount
|
||||
amount={item.amount}
|
||||
look="plain"
|
||||
label={false}
|
||||
showPlus={true}
|
||||
precision={8}
|
||||
/>
|
||||
<br />
|
||||
<CreditAmount
|
||||
amount={fee}
|
||||
look="plain"
|
||||
fee={true}
|
||||
label={false}
|
||||
precision={8}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{this.getClaimLink(item.claim_name, item.claim_id)}
|
||||
</td>
|
||||
<td>
|
||||
<LinkTransaction id={txid} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: <tr key={txid}>
|
||||
<td>
|
||||
{date
|
||||
? date.toLocaleDateString("en-US", options)
|
||||
: <span className="empty">
|
||||
{__("(Transaction pending)")}
|
||||
</span>}
|
||||
</td>
|
||||
<td>
|
||||
<CreditAmount
|
||||
amount={transaction.amount}
|
||||
look="plain"
|
||||
label={false}
|
||||
showPlus={true}
|
||||
precision={8}
|
||||
/>
|
||||
<br />
|
||||
<CreditAmount
|
||||
amount={fee}
|
||||
look="plain"
|
||||
fee={true}
|
||||
label={false}
|
||||
precision={8}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<LinkTransaction id={txid} />
|
||||
</td>
|
||||
</tr>;
|
||||
}
|
||||
|
||||
removeFeeTx(transaction) {
|
||||
if (this.props.filter == "unfiltered")
|
||||
return Math.abs(transaction.amount) != Math.abs(transaction.fee);
|
||||
else return true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { transactions, filter } = this.props;
|
||||
let transactionList = transactions
|
||||
.filter(this.filterList, this)
|
||||
.filter(this.removeFeeTx, this)
|
||||
.map(this.renderBody, this);
|
||||
|
||||
if (transactionList.length == 0) {
|
||||
return (
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="empty" colSpan="3">
|
||||
{__("There are no transactions of this type.")}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
{transactionList}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionTableBody;
|
|
@ -1,19 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
class TransactionTableHeader extends React.PureComponent {
|
||||
render() {
|
||||
const { filter } = this.props;
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{__("Date")}</th>
|
||||
<th>{__("Amount(Fee)")}</th>
|
||||
{filter != "unfiltered" && <th> {__("Claim Name")} </th>}
|
||||
<th>{__("Transaction")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionTableHeader;
|
|
@ -0,0 +1,78 @@
|
|||
import React from "react";
|
||||
import LinkTransaction from "component/linkTransaction";
|
||||
import { CreditAmount } from "component/common";
|
||||
import DateTime from "component/dateTime";
|
||||
import Link from "component/link";
|
||||
import lbryuri from "lbryuri";
|
||||
|
||||
class TransactionListItem extends React.PureComponent {
|
||||
render() {
|
||||
const { reward, transaction } = this.props;
|
||||
const {
|
||||
amount,
|
||||
claim_id: claimId,
|
||||
claim_name: name,
|
||||
date,
|
||||
fee,
|
||||
txid,
|
||||
type,
|
||||
} = transaction;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
{date
|
||||
? <div>
|
||||
<DateTime date={date} show={DateTime.SHOW_DATE} />
|
||||
<div className="meta">
|
||||
<DateTime date={date} show={DateTime.SHOW_TIME} />
|
||||
</div>
|
||||
</div>
|
||||
: <span className="empty">
|
||||
{__("(Transaction pending)")}
|
||||
</span>}
|
||||
</td>
|
||||
<td>
|
||||
<CreditAmount
|
||||
amount={amount}
|
||||
look="plain"
|
||||
label={false}
|
||||
showPlus={true}
|
||||
precision={8}
|
||||
/>
|
||||
<br />
|
||||
{fee != 0 &&
|
||||
<CreditAmount
|
||||
amount={fee}
|
||||
look="fee"
|
||||
label={false}
|
||||
precision={8}
|
||||
/>}
|
||||
</td>
|
||||
<td>
|
||||
{type}
|
||||
</td>
|
||||
<td>
|
||||
{reward &&
|
||||
<Link navigate="/rewards">
|
||||
{__("Reward: %s", reward.reward_title)}
|
||||
</Link>}
|
||||
{name &&
|
||||
claimId &&
|
||||
<Link
|
||||
className="button-text"
|
||||
navigate="/show"
|
||||
navigateParams={{ uri: lbryuri.build({ name, claimId }) }}
|
||||
>
|
||||
{name}
|
||||
</Link>}
|
||||
</td>
|
||||
<td>
|
||||
<LinkTransaction id={txid} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TransactionListItem;
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import TransactionTableHeader from "./internal/TransactionListHeader";
|
||||
import TransactionTableBody from "./internal/TransactionListBody";
|
||||
import TransactionListItem from "./internal/TransactionListItem";
|
||||
import FormField from "component/formField";
|
||||
|
||||
class TransactionList extends React.PureComponent {
|
||||
|
@ -8,7 +7,7 @@ class TransactionList extends React.PureComponent {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: "unfiltered",
|
||||
filter: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -18,45 +17,63 @@ class TransactionList extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
handleClaimNameClicked(uri) {
|
||||
this.props.navigate("/show", { uri });
|
||||
filterTransaction(transaction) {
|
||||
const { filter } = this.state;
|
||||
|
||||
return !filter || filter == transaction.type;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { emptyMessage, transactions } = this.props;
|
||||
const { filter } = this.state;
|
||||
const { emptyMessage, rewards, transactions } = this.props;
|
||||
|
||||
if (!transactions || !transactions.length) {
|
||||
return (
|
||||
<div className="empty">
|
||||
{emptyMessage || __("No transactions to list.")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let transactionList = transactions.filter(
|
||||
this.filterTransaction.bind(this)
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="sort-section">
|
||||
{__("Filter")} {" "}
|
||||
<FormField
|
||||
type="select"
|
||||
onChange={this.handleFilterChanged.bind(this)}
|
||||
>
|
||||
<option value="unfiltered">{__("All")}</option>
|
||||
<option value="claim">{__("Publishes")}</option>
|
||||
<option value="support">{__("Supports")}</option>
|
||||
<option value="tipSupport">{__("Tips")}</option>
|
||||
<option value="update">{__("Updates")}</option>
|
||||
</FormField>
|
||||
</span>
|
||||
<table className="table-standard table-stretch">
|
||||
<TransactionTableHeader filter={filter} />
|
||||
<TransactionTableBody
|
||||
transactions={transactions}
|
||||
filter={filter}
|
||||
navigate={this.handleClaimNameClicked.bind(this)}
|
||||
/>
|
||||
</table>
|
||||
{(transactionList.length || this.state.filter) &&
|
||||
<span className="sort-section">
|
||||
{__("Filter")} {" "}
|
||||
<FormField
|
||||
type="select"
|
||||
onChange={this.handleFilterChanged.bind(this)}
|
||||
>
|
||||
<option value="">{__("All")}</option>
|
||||
<option value="spend">{__("Spends")}</option>
|
||||
<option value="receive">{__("Receives")}</option>
|
||||
<option value="publish">{__("Publishes")}</option>
|
||||
<option value="channel">{__("Channels")}</option>
|
||||
<option value="tip">{__("Tips")}</option>
|
||||
<option value="support">{__("Supports")}</option>
|
||||
<option value="update">{__("Updates")}</option>
|
||||
</FormField>
|
||||
</span>}
|
||||
{!transactionList.length &&
|
||||
<div className="empty">
|
||||
{emptyMessage || __("No transactions to list.")}
|
||||
</div>}
|
||||
{Boolean(transactionList.length) &&
|
||||
<table className="table-standard table-transactions table-stretch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{__("Date")}</th>
|
||||
<th>{__("Amount (Fee)")}</th>
|
||||
<th>{__("Type")} </th>
|
||||
<th>{__("Details")} </th>
|
||||
<th>{__("Transaction")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactionList.map(t =>
|
||||
<TransactionListItem
|
||||
key={`${t.txid}:${t.nout}`}
|
||||
transaction={t}
|
||||
reward={rewards && rewards[t.txid]}
|
||||
/>
|
||||
)}
|
||||
</tbody>
|
||||
</table>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -215,11 +215,7 @@ class DiscoverPage extends React.PureComponent {
|
|||
failedToLoad = !fetchingFeaturedUris && !hasContent;
|
||||
|
||||
return (
|
||||
<main
|
||||
className={
|
||||
hasContent && fetchingFeaturedUris ? "main--refreshing" : null
|
||||
}
|
||||
>
|
||||
<main className={hasContent && fetchingFeaturedUris ? "reloading" : null}>
|
||||
{!hasContent &&
|
||||
fetchingFeaturedUris &&
|
||||
<BusyMessage message={__("Fetching content")} />}
|
||||
|
|
|
@ -10,18 +10,26 @@ class TransactionHistoryPage extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { fetchingTransactions, transactions } = this.props;
|
||||
|
||||
return (
|
||||
<main className="main--single-column">
|
||||
<SubHeader />
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<div
|
||||
className={
|
||||
"card__title-primary " +
|
||||
(fetchingTransactions && transactions.length ? "reloading" : "")
|
||||
}
|
||||
>
|
||||
<h3>{__("Transaction History")}</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{fetchingTransactions &&
|
||||
<BusyMessage message={__("Loading transactions")} />}
|
||||
{!fetchingTransactions &&
|
||||
<TransactionList transactions={transactions} />}
|
||||
{fetchingTransactions && !transactions.length
|
||||
? <BusyMessage message={__("Loading transactions")} />
|
||||
: ""}
|
||||
{transactions && transactions.length
|
||||
? <TransactionList transactions={transactions} />
|
||||
: ""}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
@ -10,7 +10,7 @@ const buildDraftTransaction = () => ({
|
|||
const defaultState = {
|
||||
balance: undefined,
|
||||
blocks: {},
|
||||
transactions: [],
|
||||
transactions: {},
|
||||
fetchingTransactions: false,
|
||||
receiveAddress: address,
|
||||
gettingNewAddress: false,
|
||||
|
@ -25,20 +25,16 @@ reducers[types.FETCH_TRANSACTIONS_STARTED] = function(state, action) {
|
|||
};
|
||||
|
||||
reducers[types.FETCH_TRANSACTIONS_COMPLETED] = function(state, action) {
|
||||
const oldTransactions = Object.assign({}, state.transactions);
|
||||
const byId = Object.assign({}, oldTransactions.byId);
|
||||
let byId = Object.assign({}, state.transactions);
|
||||
|
||||
const { transactions } = action.data;
|
||||
|
||||
transactions.forEach(transaction => {
|
||||
byId[transaction.txid] = transaction;
|
||||
});
|
||||
|
||||
const newTransactions = Object.assign({}, oldTransactions, {
|
||||
byId: byId,
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
transactions: newTransactions,
|
||||
transactions: byId,
|
||||
fetchingTransactions: false,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -19,6 +19,15 @@ export const selectClaimedRewards = createSelector(
|
|||
byId => Object.values(byId) || []
|
||||
);
|
||||
|
||||
export const selectClaimedRewardsByTransactionId = createSelector(
|
||||
selectClaimedRewards,
|
||||
rewards =>
|
||||
rewards.reduce((map, reward) => {
|
||||
map[reward.transaction_id] = reward;
|
||||
return map;
|
||||
}, {})
|
||||
);
|
||||
|
||||
export const selectUnclaimedRewards = createSelector(
|
||||
selectUnclaimedRewardsByType,
|
||||
byType =>
|
||||
|
|
|
@ -7,34 +7,70 @@ export const selectBalance = createSelector(
|
|||
state => state.balance
|
||||
);
|
||||
|
||||
export const selectTransactions = createSelector(
|
||||
_selectState,
|
||||
state => state.transactions || {}
|
||||
);
|
||||
|
||||
export const selectTransactionsById = createSelector(
|
||||
selectTransactions,
|
||||
transactions => transactions.byId || {}
|
||||
_selectState,
|
||||
state => state.transactions
|
||||
);
|
||||
|
||||
export const selectTransactionItems = createSelector(
|
||||
selectTransactionsById,
|
||||
byId => {
|
||||
const transactionItems = [];
|
||||
const txids = Object.keys(byId);
|
||||
txids.forEach(txid => {
|
||||
const items = [];
|
||||
|
||||
Object.keys(byId).forEach(txid => {
|
||||
const tx = byId[txid];
|
||||
transactionItems.push({
|
||||
id: txid,
|
||||
date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null,
|
||||
amount: parseFloat(tx.value),
|
||||
claim_info: tx.claim_info,
|
||||
support_info: tx.support_info,
|
||||
update_info: tx.update_info,
|
||||
fee: tx.fee,
|
||||
});
|
||||
|
||||
//ignore dust/fees
|
||||
if (Math.abs(tx.amount) === Math.abs(tx.fee)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let append = [];
|
||||
|
||||
append.push(
|
||||
...tx.claim_info.map(item =>
|
||||
Object.assign({}, item, {
|
||||
type: item.claim_name[0] === "@" ? "channel" : "publish",
|
||||
})
|
||||
)
|
||||
);
|
||||
append.push(
|
||||
...tx.support_info.map(item =>
|
||||
Object.assign({}, item, { type: !item.is_tip ? "support" : "tip" })
|
||||
)
|
||||
);
|
||||
append.push(
|
||||
...tx.update_info.map(item =>
|
||||
Object.assign({}, item, { type: "update" })
|
||||
)
|
||||
);
|
||||
|
||||
if (!append.length) {
|
||||
append.push(
|
||||
Object.assign({}, tx, {
|
||||
type: tx.value < 0 ? "spend" : "receive",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
items.push(
|
||||
...append.map(item => {
|
||||
const amount = parseFloat(item.value || -1 * item.amount); //it's value on a transaction, amount on an outpoint (which has the sign the opposite way)
|
||||
|
||||
return {
|
||||
txid: txid,
|
||||
date: tx.timestamp ? new Date(parseInt(tx.timestamp) * 1000) : null,
|
||||
amount: amount,
|
||||
fee: amount < 0 ? -1 * tx.fee / append.length : 0,
|
||||
claim_id: item.claim_id,
|
||||
claim_name: item.claim_name,
|
||||
type: item.type || "send",
|
||||
nout: item.nout,
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
return transactionItems.reverse();
|
||||
return items.reverse();
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -27,6 +27,11 @@ body
|
|||
font-weight: bold;
|
||||
color: var(--color-money);
|
||||
}
|
||||
.credit-amount--fee
|
||||
{
|
||||
font-size: 0.9em;
|
||||
color: var(--color-meta-light);
|
||||
}
|
||||
|
||||
#main-content
|
||||
{
|
||||
|
@ -44,7 +49,8 @@ body
|
|||
width: $width-page-constrained;
|
||||
}
|
||||
}
|
||||
main.main--refreshing {
|
||||
|
||||
.reloading {
|
||||
&:before {
|
||||
$width: 30px;
|
||||
position: absolute;
|
||||
|
|
|
@ -60,3 +60,11 @@ table.table-standard {
|
|||
table.table-stretch {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.table-transactions {
|
||||
td:nth-of-type(1) { width: 15%; }
|
||||
td:nth-of-type(2) { width: 15%; }
|
||||
td:nth-of-type(3) { width: 15%; }
|
||||
td:nth-of-type(4) { width: 40%; }
|
||||
td:nth-of-type(5) { width: 15%; }
|
||||
}
|
Loading…
Add table
Reference in a new issue