Compare commits
No commits in common. "59f35d0366a6d2e774a3b39b3e9d24b468c60a4c" and "4980b2e33b44f0d7da2a17e650946d929564f286" have entirely different histories.
59f35d0366
...
4980b2e33b
|
@ -57,13 +57,6 @@ message UpdateSkillSpecializationsRequest {
|
|||
repeated string specializations = 2;
|
||||
}
|
||||
|
||||
//Update all merits on the character sheet by overwriting them.
|
||||
//Primarily for the web UI.
|
||||
message UpdateMeritsRequest {
|
||||
CharacterIdentifier character = 1;
|
||||
repeated CofdSheet.Merit merits = 2;
|
||||
}
|
||||
|
||||
//Add a Condition to a Chronicles of Darkness character sheet.
|
||||
message AddConditionRequest {
|
||||
string character_username = 1;
|
||||
|
|
|
@ -208,34 +208,6 @@ export namespace UpdateSkillSpecializationsRequest {
|
|||
}
|
||||
}
|
||||
|
||||
export class UpdateMeritsRequest extends jspb.Message {
|
||||
hasCharacter(): boolean;
|
||||
clearCharacter(): void;
|
||||
getCharacter(): CharacterIdentifier | undefined;
|
||||
setCharacter(value?: CharacterIdentifier): void;
|
||||
|
||||
clearMeritsList(): void;
|
||||
getMeritsList(): Array<cofd_pb.CofdSheet.Merit>;
|
||||
setMeritsList(value: Array<cofd_pb.CofdSheet.Merit>): void;
|
||||
addMerits(value?: cofd_pb.CofdSheet.Merit, index?: number): cofd_pb.CofdSheet.Merit;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): UpdateMeritsRequest.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: UpdateMeritsRequest): UpdateMeritsRequest.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: UpdateMeritsRequest, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): UpdateMeritsRequest;
|
||||
static deserializeBinaryFromReader(message: UpdateMeritsRequest, reader: jspb.BinaryReader): UpdateMeritsRequest;
|
||||
}
|
||||
|
||||
export namespace UpdateMeritsRequest {
|
||||
export type AsObject = {
|
||||
character?: CharacterIdentifier.AsObject,
|
||||
meritsList: Array<cofd_pb.CofdSheet.Merit.AsObject>,
|
||||
}
|
||||
}
|
||||
|
||||
export class AddConditionRequest extends jspb.Message {
|
||||
getCharacterUsername(): string;
|
||||
setCharacterUsername(value: string): void;
|
||||
|
|
|
@ -22,7 +22,6 @@ goog.exportSymbol('proto.models.proto.cofd.api.CharacterIdentifier', null, globa
|
|||
goog.exportSymbol('proto.models.proto.cofd.api.RemoveConditionRequest', null, global);
|
||||
goog.exportSymbol('proto.models.proto.cofd.api.UpdateAttributeRequest', null, global);
|
||||
goog.exportSymbol('proto.models.proto.cofd.api.UpdateBasicInfoRequest', null, global);
|
||||
goog.exportSymbol('proto.models.proto.cofd.api.UpdateMeritsRequest', null, global);
|
||||
goog.exportSymbol('proto.models.proto.cofd.api.UpdateSkillRequest', null, global);
|
||||
goog.exportSymbol('proto.models.proto.cofd.api.UpdateSkillSpecializationsRequest', null, global);
|
||||
goog.exportSymbol('proto.models.proto.cofd.api.UpdateSkillValueRequest', null, global);
|
||||
|
@ -173,27 +172,6 @@ if (goog.DEBUG && !COMPILED) {
|
|||
*/
|
||||
proto.models.proto.cofd.api.UpdateSkillSpecializationsRequest.displayName = 'proto.models.proto.cofd.api.UpdateSkillSpecializationsRequest';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
* server response, or constructed directly in Javascript. The array is used
|
||||
* in place and becomes part of the constructed object. It is not cloned.
|
||||
* If no data is provided, the constructed object will be empty, but still
|
||||
* valid.
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, proto.models.proto.cofd.api.UpdateMeritsRequest.repeatedFields_, null);
|
||||
};
|
||||
goog.inherits(proto.models.proto.cofd.api.UpdateMeritsRequest, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.displayName = 'proto.models.proto.cofd.api.UpdateMeritsRequest';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
|
@ -1668,217 +1646,6 @@ proto.models.proto.cofd.api.UpdateSkillSpecializationsRequest.prototype.clearSpe
|
|||
|
||||
|
||||
|
||||
/**
|
||||
* List of repeated fields within this message type.
|
||||
* @private {!Array<number>}
|
||||
* @const
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.repeatedFields_ = [2];
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||
* Optional fields that are not set will be set to undefined.
|
||||
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||
* For the list of reserved names please see:
|
||||
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||
* JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.models.proto.cofd.api.UpdateMeritsRequest.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Static version of the {@see toObject} method.
|
||||
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||
* the JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.models.proto.cofd.api.UpdateMeritsRequest} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
character: (f = msg.getCharacter()) && proto.models.proto.cofd.api.CharacterIdentifier.toObject(includeInstance, f),
|
||||
meritsList: jspb.Message.toObjectList(msg.getMeritsList(),
|
||||
cofd_pb.CofdSheet.Merit.toObject, includeInstance)
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
obj.$jspbMessageInstance = msg;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.models.proto.cofd.api.UpdateMeritsRequest}
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.models.proto.cofd.api.UpdateMeritsRequest;
|
||||
return proto.models.proto.cofd.api.UpdateMeritsRequest.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.models.proto.cofd.api.UpdateMeritsRequest} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.models.proto.cofd.api.UpdateMeritsRequest}
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
}
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = new proto.models.proto.cofd.api.CharacterIdentifier;
|
||||
reader.readMessage(value,proto.models.proto.cofd.api.CharacterIdentifier.deserializeBinaryFromReader);
|
||||
msg.setCharacter(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = new cofd_pb.CofdSheet.Merit;
|
||||
reader.readMessage(value,cofd_pb.CofdSheet.Merit.deserializeBinaryFromReader);
|
||||
msg.addMerits(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.models.proto.cofd.api.UpdateMeritsRequest} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getCharacter();
|
||||
if (f != null) {
|
||||
writer.writeMessage(
|
||||
1,
|
||||
f,
|
||||
proto.models.proto.cofd.api.CharacterIdentifier.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
f = message.getMeritsList();
|
||||
if (f.length > 0) {
|
||||
writer.writeRepeatedMessage(
|
||||
2,
|
||||
f,
|
||||
cofd_pb.CofdSheet.Merit.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional CharacterIdentifier character = 1;
|
||||
* @return {?proto.models.proto.cofd.api.CharacterIdentifier}
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.getCharacter = function() {
|
||||
return /** @type{?proto.models.proto.cofd.api.CharacterIdentifier} */ (
|
||||
jspb.Message.getWrapperField(this, proto.models.proto.cofd.api.CharacterIdentifier, 1));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {?proto.models.proto.cofd.api.CharacterIdentifier|undefined} value
|
||||
* @return {!proto.models.proto.cofd.api.UpdateMeritsRequest} returns this
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.setCharacter = function(value) {
|
||||
return jspb.Message.setWrapperField(this, 1, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the message field making it undefined.
|
||||
* @return {!proto.models.proto.cofd.api.UpdateMeritsRequest} returns this
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.clearCharacter = function() {
|
||||
return this.setCharacter(undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.hasCharacter = function() {
|
||||
return jspb.Message.getField(this, 1) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* repeated models.proto.cofd.CofdSheet.Merit merits = 2;
|
||||
* @return {!Array<!proto.models.proto.cofd.CofdSheet.Merit>}
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.getMeritsList = function() {
|
||||
return /** @type{!Array<!proto.models.proto.cofd.CofdSheet.Merit>} */ (
|
||||
jspb.Message.getRepeatedWrapperField(this, cofd_pb.CofdSheet.Merit, 2));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {!Array<!proto.models.proto.cofd.CofdSheet.Merit>} value
|
||||
* @return {!proto.models.proto.cofd.api.UpdateMeritsRequest} returns this
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.setMeritsList = function(value) {
|
||||
return jspb.Message.setRepeatedWrapperField(this, 2, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {!proto.models.proto.cofd.CofdSheet.Merit=} opt_value
|
||||
* @param {number=} opt_index
|
||||
* @return {!proto.models.proto.cofd.CofdSheet.Merit}
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.addMerits = function(opt_value, opt_index) {
|
||||
return jspb.Message.addToRepeatedWrapperField(this, 2, opt_value, proto.models.proto.cofd.CofdSheet.Merit, opt_index);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the list making it empty but non-null.
|
||||
* @return {!proto.models.proto.cofd.api.UpdateMeritsRequest} returns this
|
||||
*/
|
||||
proto.models.proto.cofd.api.UpdateMeritsRequest.prototype.clearMeritsList = function() {
|
||||
return this.setMeritsList([]);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import * as jspb from "google-protobuf";
|
||||
import { ApiResult, UpdateBasicInfoRequest, UpdateAttributeRequest, UpdateSkillValueRequest, UpdateMeritsRequest } from "../_proto/cofd_api_pb";
|
||||
import { ApiResult, UpdateBasicInfoRequest, UpdateAttributeRequest, UpdateSkillValueRequest } from "../_proto/cofd_api_pb";
|
||||
|
||||
const PROTOBUF_CONTENT_TYPE = { 'Content-Type': 'application/x-protobuf' };
|
||||
|
||||
function staticImplements<T>() {
|
||||
return <U extends T>(constructor: U) => { constructor };
|
||||
}
|
||||
|
||||
async function makeRequest<T extends jspb.Message>(uri: string, params: T): Promise<Uint8Array> {
|
||||
let resp = await fetch(uri, {
|
||||
method: 'POST',
|
||||
|
@ -28,8 +32,3 @@ export async function updateBasicInfo(params: UpdateBasicInfoRequest): Promise<A
|
|||
let data = await makeRequest('/api/rpc/cofd/update_basic_info', params);
|
||||
return ApiResult.deserializeBinary(data);
|
||||
}
|
||||
|
||||
export async function updateMerits(params: UpdateMeritsRequest): Promise<ApiResult> {
|
||||
let data = await makeRequest('/api/rpc/cofd/update_merits', params);
|
||||
return ApiResult.deserializeBinary(data);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { CharacterIdentifier, UpdateBasicInfoRequest, UpdateSkillValueRequest, UpdateAttributeRequest, UpdateMeritsRequest } from "../../_proto/cofd_api_pb";
|
||||
import { CofdSheet } from "../../_proto/cofd_pb";
|
||||
import { CharacterIdentifier, UpdateBasicInfoRequest, UpdateSkillValueRequest, UpdateAttributeRequest } from "../../_proto/cofd_api_pb";
|
||||
import * as api from "../api";
|
||||
|
||||
// This is the scripting for the edit character page, which submits
|
||||
|
@ -95,94 +94,9 @@ import * as api from "../api";
|
|||
});
|
||||
}
|
||||
|
||||
function setupMerits() {
|
||||
async function updateMerits() {
|
||||
let meritElements = document.querySelectorAll<HTMLInputElement>('#merits input[class="merit-name"]');
|
||||
let merits: CofdSheet.Merit[] =
|
||||
Array.from(meritElements).map(input => {
|
||||
const dotsInput = input.parentElement?.querySelector<HTMLInputElement>('input[class="merit-dots"]');
|
||||
|
||||
let dotsAmount = dotsInput?.value ?? "0";
|
||||
if (dotsAmount.length == 0) { dotsAmount = "0"; }
|
||||
|
||||
const merit = new CofdSheet.Merit();
|
||||
merit.setName(input.value);
|
||||
merit.setDots(parseInt(dotsAmount));
|
||||
|
||||
return merit;
|
||||
});
|
||||
|
||||
const params = new UpdateMeritsRequest();
|
||||
params.setCharacter(characterId());
|
||||
params.setMeritsList(merits);
|
||||
|
||||
let resp = await api.updateMerits(params);
|
||||
console.log("got a response back", resp);
|
||||
}
|
||||
|
||||
async function removeMerit(event: Event) {
|
||||
const button = event.target as Option<Element>;
|
||||
const meritLine = button?.parentElement;
|
||||
if (meritLine) {
|
||||
document.querySelector("#merit-list")?.removeChild(meritLine);
|
||||
await updateMerits();
|
||||
}
|
||||
}
|
||||
|
||||
function addMeritLine() {
|
||||
const meritName = document.createElement('input');
|
||||
meritName.type = 'text';
|
||||
meritName.value = '';
|
||||
meritName.classList.add('merit-name');
|
||||
|
||||
const meritDots = document.createElement('input');
|
||||
meritDots.type = 'number';
|
||||
meritDots.min = '0';
|
||||
meritDots.value = '0';
|
||||
meritDots.classList.add('merit-dots');
|
||||
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.innerText = "X";
|
||||
removeButton.setAttribute('data-event-type', 'remove-merit');
|
||||
|
||||
const dots = document.createElement('span');
|
||||
dots.innerText = 'Dots';
|
||||
|
||||
const merit = document.createElement('div');
|
||||
merit.classList.add('merit');
|
||||
merit.appendChild(meritName);
|
||||
merit.appendChild(meritDots);
|
||||
merit.appendChild(dots);
|
||||
merit.appendChild(removeButton);
|
||||
|
||||
document.querySelector<HTMLDivElement>("#merit-list")?.appendChild(merit);
|
||||
}
|
||||
|
||||
async function processControlEvent(event: Event) {
|
||||
console.log('processing merit control event');
|
||||
const element = event.target as Element;
|
||||
const eventType = element.getAttribute('data-event-type') ?? '';
|
||||
|
||||
if (eventType == 'remove-merit') {
|
||||
await removeMerit(event);
|
||||
} else if (eventType == 'add-merit') {
|
||||
addMeritLine();
|
||||
}
|
||||
}
|
||||
|
||||
const meritControls = document.querySelector<HTMLDivElement>("#merit-controls");
|
||||
meritControls?.addEventListener('click', processControlEvent);
|
||||
|
||||
const meritList = document.querySelector<HTMLDivElement>("#merit-list");
|
||||
meritList?.addEventListener('click', processControlEvent);
|
||||
meritList?.addEventListener('blur', updateMerits);
|
||||
meritList?.addEventListener('change', updateMerits);
|
||||
}
|
||||
|
||||
setupAttributes();
|
||||
setupSkills();
|
||||
setupBasicInfo();
|
||||
setupMerits();
|
||||
})().catch(e => {
|
||||
alert(e);
|
||||
});
|
||||
|
|
|
@ -156,18 +156,5 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="merits">
|
||||
<h2>Merits</h2>
|
||||
<div id="merit-list">
|
||||
{% for merit in sheet.merits %}
|
||||
{{ macros::merit(name=merit.name, value=merit.dots) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="merit-controls">
|
||||
<button data-event-type="add-merit">Add Merit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -11,11 +11,3 @@
|
|||
<input id="{{name}}" name="{{name}}" type="number" min="0" value="{{value}}" />
|
||||
</div>
|
||||
{% endmacro skill %}
|
||||
|
||||
{% macro merit(name, value) %}
|
||||
<div class="merit">
|
||||
<input type="text" class="merit-name" value="{{name}}" />
|
||||
<input type="number" class="merit-dots" min="0" value="{{value}}" /> <span>Dots</span>
|
||||
<button data-event-type="remove-merit">X</button>
|
||||
</div>
|
||||
{% endmacro merit %}
|
||||
|
|
|
@ -11,7 +11,6 @@ pub(crate) fn routes() -> Vec<rocket::Route> {
|
|||
cofd::update_attribute_value,
|
||||
cofd::update_skill,
|
||||
cofd::update_skill_value,
|
||||
cofd::update_merits,
|
||||
cofd::add_condition,
|
||||
cofd::remove_condition
|
||||
]
|
||||
|
|
|
@ -163,30 +163,6 @@ pub(super) async fn update_skill_value<'a>(
|
|||
}))
|
||||
}
|
||||
|
||||
#[post("/rpc/cofd/update_merits", data = "<req>")]
|
||||
pub(super) async fn update_merits<'a>(
|
||||
req: Proto<UpdateMeritsRequest>,
|
||||
conn: TenebrousDbConn<'_>,
|
||||
logged_in_user: Option<&User>,
|
||||
) -> Result<Proto<ApiResult>, Error> {
|
||||
let mut character = load_character(
|
||||
&conn,
|
||||
logged_in_user,
|
||||
&req.character.owner(),
|
||||
req.character.id(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut sheet: CofdSheet = character.try_deserialize()?;
|
||||
sheet.merits = req.merits.clone();
|
||||
|
||||
println!("updated merits");
|
||||
|
||||
character.update_data(&sheet)?;
|
||||
conn.update_character_sheet(&character).await?;
|
||||
Ok(Proto(ApiResult::success()))
|
||||
}
|
||||
|
||||
#[put("/rpc/cofd/add_condition", data = "<info>")]
|
||||
pub(super) fn add_condition<'a>(info: Proto<AddConditionRequest>) -> &'a str {
|
||||
"lol"
|
||||
|
|
Loading…
Reference in New Issue