<commit_before>const router = require('express').Router()

const { setLocalNav, setDefaultQuery, redirectToFirstNavItem } = require('../middleware')
const { shared } = require('./middleware')
const {
  getBriefInvestmentSummary,
  expandTeamMembers,
} = require('./middleware/team')

const {
  create,
  archive,
  audit,
  details,
  edit,
  evaluation,
  team,
} = require('./controllers')

const {
  clientRelationshipManagementFormMiddleware,
  detailsFormMiddleware,
  investmentTypeFormMiddleware,
  projectManagementFormMiddleware,
  projectStageFormMiddleware,
  requirementsFormMiddleware,
  valueFormMiddleware,
  teamMembersFormMiddleware,
} = require('./middleware/forms')

const { renderInvestmentList } = require('./controllers/list')
const { renderInteractionList } = require('./controllers/interactions')

const { getInvestmentProjectsCollection, getRequestBody } = require('./middleware/collection')
const { setInteractionsReturnUrl, setInteractionsEntityName, setCompanyDetails } = require('./middleware/interactions')

const {
  renderStatusPage,
  postStatus,
} = require('./controllers/status')

const {
  selectUKCompany,
  searchForUKCompany,
  renderCompanyResults,
  removeUKCompany,
} = require('./controllers/ukcompany')

const {
  selectAssociatedInvestmentProject,
  searchForAssociatedInvestmentProject,
  renderAssociatedInvestmentProjectResults,
  removeAssociatedInvestmentProject,
} = require('./controllers/associated')

const interactionsRouter = require('../interactions/router.sub-app')

const LOCAL_NAV = [
  { path: 'details', label: 'Project details' },
  { path: 'team', label: 'Project team' },
  { path: 'interactions', label: 'Interactions' },
  { path: 'evaluation', label: 'Evaluations' },
  { path: 'audit', label: 'Audit history' },
]

const currentYear = (new Date()).getFullYear()
const DEFAULT_COLLECTION_QUERY = {
  estimated_land_date_after: `${currentYear}-04-05`,
  estimated_land_date_before: `${currentYear + 1}-04-06`,
  sortby: 'estimated_land_date:asc',
}

router.use('/:investmentId/', setLocalNav(LOCAL_NAV))

router.param('investmentId', shared.getInvestmentDetails)
router.param('companyId', shared.getCompanyDetails)

router.get('/', setDefaultQuery(DEFAULT_COLLECTION_QUERY), getRequestBody, getInvestmentProjectsCollection, renderInvestmentList)

router.post('/:investmentId/details', archive.archiveInvestmentProjectHandler, details.detailsGetHandler)
router.get('/:investmentId/unarchive', archive.unarchiveInvestmentProjectHandler)

router.get('/:investmentId/audit', audit.getInvestmentAudit)

router.get('/create/:companyId?', create.start.redirectHandler, create.start.renderCreatePage)

router.get('/create/investment-type/info', create.investmentType.renderInvestmentInfoPage)

router
  .route('/create/investment-type/:companyId')
  .get(investmentTypeFormMiddleware.populateForm, create.investmentType.renderInvestmentTypePage)
  .post(
    investmentTypeFormMiddleware.validateForm,
    create.investmentType.postHandler,
    investmentTypeFormMiddleware.populateForm,
    create.investmentType.renderInvestmentTypePage
  )

router
  .route('/create/equity-source/:companyId')
  .get(create.equitySource.getHandler, create.equitySource.renderEquitySourcePage)
  .post(
    create.equitySource.postHandler,
    create.equitySource.getHandler,
    create.equitySource.renderEquitySourcePage
  )

router
  .route('/create/project/:equityCompanyId')
  .get(
    detailsFormMiddleware.populateForm,
    create.project.getHandler,
    create.project.renderCreateProjectPage
  )
  .post(
    detailsFormMiddleware.populateForm,
    detailsFormMiddleware.handleFormPost,
    detailsFormMiddleware.validateForm,
    create.project.postHandler,
    create.project.getHandler,
    create.project.renderCreateProjectPage
  )

router.get('/:investmentId', redirectToFirstNavItem)
router.get('/:investmentId/details', details.detailsGetHandler)

router
  .route('/:investmentId/edit-details')
  .get(detailsFormMiddleware.populateForm, edit.editDetailsGet)
  .post(detailsFormMiddleware.populateForm, detailsFormMiddleware.handleFormPost, edit.editDetailsPost, edit.editDetailsGet)

router
  .route('/:investmentId/edit-value')
  .get(valueFormMiddleware.populateForm, edit.editValueGet)
  .post(valueFormMiddleware.populateForm, valueFormMiddleware.handleFormPost, edit.editValuePost)

router
  .route('/:investmentId/edit-requirements')
  .get(requirementsFormMiddleware.populateForm, edit.editRequirementsGet)
  .post(requirementsFormMiddleware.populateForm, requirementsFormMiddleware.handleFormPost, edit.editRequirementsPost)

router.get('/:investmentId/team', expandTeamMembers, team.details.getDetailsHandler)

router
  .route('/:investmentId/edit-project-management')
  .get(getBriefInvestmentSummary, projectManagementFormMiddleware.populateForm, team.editProjectManagement.getHandler)
  .post(
    getBriefInvestmentSummary,
    projectManagementFormMiddleware.populateForm,
    projectManagementFormMiddleware.handleFormPost,
    team.editProjectManagement.postHandler,
    team.editProjectManagement.getHandler
  )

router
  .route('/:investmentId/edit-client-relationship-management')
  .get(
    clientRelationshipManagementFormMiddleware.populateForm,
    team.editClientRelationshipManagement.getHandler
  )
  .post(
    clientRelationshipManagementFormMiddleware.populateForm,
    clientRelationshipManagementFormMiddleware.handleFormPost,
    team.editClientRelationshipManagement.postHandler,
    team.editClientRelationshipManagement.getHandler
  )

router
  .route('/:investmentId/edit-team-members')
  .get(
    teamMembersFormMiddleware.populateForm,
    team.editTeamMembers.getHandler
  )
  .post(
    teamMembersFormMiddleware.populateForm,
    teamMembersFormMiddleware.handleFormPost,
    team.editTeamMembers.postHandler,
    team.editTeamMembers.getHandler
  )

router.get('/:investmentId/interactions', setInteractionsReturnUrl, renderInteractionList)

router.get('/:investmentId/evaluation', evaluation.renderEvaluationPage)

router.post('/:investmentId/change-project-stage', projectStageFormMiddleware.handleFormPost)

router.use('/:investmentId', setInteractionsReturnUrl, setInteractionsEntityName, setCompanyDetails, interactionsRouter)

router
  .route('/:investmentId/status')
  .post(postStatus, renderStatusPage)
  .get(renderStatusPage)

router.get('/:investmentId/edit-ukcompany', selectUKCompany, searchForUKCompany, renderCompanyResults)
router.get('/:investmentId/remove-ukcompany', removeUKCompany)

router.get('/:investmentId/edit-associated',
  selectAssociatedInvestmentProject,
  searchForAssociatedInvestmentProject,
  renderAssociatedInvestmentProjectResults
)

router.get('/:investmentId/remove-associated', removeAssociatedInvestmentProject)

module.exports = router
<commit_msg>Add reminder to review invest association flow<commit_after>const router = require('express').Router()

const { setLocalNav, setDefaultQuery, redirectToFirstNavItem } = require('../middleware')
const { shared } = require('./middleware')
const {
  getBriefInvestmentSummary,
  expandTeamMembers,
} = require('./middleware/team')

const {
  create,
  archive,
  audit,
  details,
  edit,
  evaluation,
  team,
} = require('./controllers')

const {
  clientRelationshipManagementFormMiddleware,
  detailsFormMiddleware,
  investmentTypeFormMiddleware,
  projectManagementFormMiddleware,
  projectStageFormMiddleware,
  requirementsFormMiddleware,
  valueFormMiddleware,
  teamMembersFormMiddleware,
} = require('./middleware/forms')

const { renderInvestmentList } = require('./controllers/list')
const { renderInteractionList } = require('./controllers/interactions')

const { getInvestmentProjectsCollection, getRequestBody } = require('./middleware/collection')
const { setInteractionsReturnUrl, setInteractionsEntityName, setCompanyDetails } = require('./middleware/interactions')

const {
  renderStatusPage,
  postStatus,
} = require('./controllers/status')

const {
  selectUKCompany,
  searchForUKCompany,
  renderCompanyResults,
  removeUKCompany,
} = require('./controllers/ukcompany')

const {
  selectAssociatedInvestmentProject,
  searchForAssociatedInvestmentProject,
  renderAssociatedInvestmentProjectResults,
  removeAssociatedInvestmentProject,
} = require('./controllers/associated')

const interactionsRouter = require('../interactions/router.sub-app')

const LOCAL_NAV = [
  { path: 'details', label: 'Project details' },
  { path: 'team', label: 'Project team' },
  { path: 'interactions', label: 'Interactions' },
  { path: 'evaluation', label: 'Evaluations' },
  { path: 'audit', label: 'Audit history' },
]

const currentYear = (new Date()).getFullYear()
const DEFAULT_COLLECTION_QUERY = {
  estimated_land_date_after: `${currentYear}-04-05`,
  estimated_land_date_before: `${currentYear + 1}-04-06`,
  sortby: 'estimated_land_date:asc',
}

router.use('/:investmentId/', setLocalNav(LOCAL_NAV))

router.param('investmentId', shared.getInvestmentDetails)
router.param('companyId', shared.getCompanyDetails)

router.get('/', setDefaultQuery(DEFAULT_COLLECTION_QUERY), getRequestBody, getInvestmentProjectsCollection, renderInvestmentList)

router.post('/:investmentId/details', archive.archiveInvestmentProjectHandler, details.detailsGetHandler)
router.get('/:investmentId/unarchive', archive.unarchiveInvestmentProjectHandler)

router.get('/:investmentId/audit', audit.getInvestmentAudit)

router.get('/create/:companyId?', create.start.redirectHandler, create.start.renderCreatePage)

router.get('/create/investment-type/info', create.investmentType.renderInvestmentInfoPage)

router
  .route('/create/investment-type/:companyId')
  .get(investmentTypeFormMiddleware.populateForm, create.investmentType.renderInvestmentTypePage)
  .post(
    investmentTypeFormMiddleware.validateForm,
    create.investmentType.postHandler,
    investmentTypeFormMiddleware.populateForm,
    create.investmentType.renderInvestmentTypePage
  )

router
  .route('/create/equity-source/:companyId')
  .get(create.equitySource.getHandler, create.equitySource.renderEquitySourcePage)
  .post(
    create.equitySource.postHandler,
    create.equitySource.getHandler,
    create.equitySource.renderEquitySourcePage
  )

router
  .route('/create/project/:equityCompanyId')
  .get(
    detailsFormMiddleware.populateForm,
    create.project.getHandler,
    create.project.renderCreateProjectPage
  )
  .post(
    detailsFormMiddleware.populateForm,
    detailsFormMiddleware.handleFormPost,
    detailsFormMiddleware.validateForm,
    create.project.postHandler,
    create.project.getHandler,
    create.project.renderCreateProjectPage
  )

router.get('/:investmentId', redirectToFirstNavItem)
router.get('/:investmentId/details', details.detailsGetHandler)

router
  .route('/:investmentId/edit-details')
  .get(detailsFormMiddleware.populateForm, edit.editDetailsGet)
  .post(detailsFormMiddleware.populateForm, detailsFormMiddleware.handleFormPost, edit.editDetailsPost, edit.editDetailsGet)

router
  .route('/:investmentId/edit-value')
  .get(valueFormMiddleware.populateForm, edit.editValueGet)
  .post(valueFormMiddleware.populateForm, valueFormMiddleware.handleFormPost, edit.editValuePost)

router
  .route('/:investmentId/edit-requirements')
  .get(requirementsFormMiddleware.populateForm, edit.editRequirementsGet)
  .post(requirementsFormMiddleware.populateForm, requirementsFormMiddleware.handleFormPost, edit.editRequirementsPost)

router.get('/:investmentId/team', expandTeamMembers, team.details.getDetailsHandler)

router
  .route('/:investmentId/edit-project-management')
  .get(getBriefInvestmentSummary, projectManagementFormMiddleware.populateForm, team.editProjectManagement.getHandler)
  .post(
    getBriefInvestmentSummary,
    projectManagementFormMiddleware.populateForm,
    projectManagementFormMiddleware.handleFormPost,
    team.editProjectManagement.postHandler,
    team.editProjectManagement.getHandler
  )

router
  .route('/:investmentId/edit-client-relationship-management')
  .get(
    clientRelationshipManagementFormMiddleware.populateForm,
    team.editClientRelationshipManagement.getHandler
  )
  .post(
    clientRelationshipManagementFormMiddleware.populateForm,
    clientRelationshipManagementFormMiddleware.handleFormPost,
    team.editClientRelationshipManagement.postHandler,
    team.editClientRelationshipManagement.getHandler
  )

router
  .route('/:investmentId/edit-team-members')
  .get(
    teamMembersFormMiddleware.populateForm,
    team.editTeamMembers.getHandler
  )
  .post(
    teamMembersFormMiddleware.populateForm,
    teamMembersFormMiddleware.handleFormPost,
    team.editTeamMembers.postHandler,
    team.editTeamMembers.getHandler
  )

router.get('/:investmentId/interactions', setInteractionsReturnUrl, renderInteractionList)

router.get('/:investmentId/evaluation', evaluation.renderEvaluationPage)

router.post('/:investmentId/change-project-stage', projectStageFormMiddleware.handleFormPost)

router.use('/:investmentId', setInteractionsReturnUrl, setInteractionsEntityName, setCompanyDetails, interactionsRouter)

router
  .route('/:investmentId/status')
  .post(postStatus, renderStatusPage)
  .get(renderStatusPage)

router.get('/:investmentId/edit-ukcompany', selectUKCompany, searchForUKCompany, renderCompanyResults)
router.get('/:investmentId/remove-ukcompany', removeUKCompany)

// Todo - DH-1030
// Currently to associate the user you must click a link in the investment details page after
// indicating there is an association with a non-fdi r&d project
// Review the investment flow later to see if this can be improved.

router.get('/:investmentId/edit-associated',
  selectAssociatedInvestmentProject,
  searchForAssociatedInvestmentProject,
  renderAssociatedInvestmentProjectResults
)

router.get('/:investmentId/remove-associated', removeAssociatedInvestmentProject)

module.exports = router
<|endoftext|>


<commit_before>const Path = require('node:path');
const ContentDisposition = require('content-disposition');
const ContentType = require('content-type');
const MediaTyper = require('media-typer');
const iconv = require('iconv-lite');
const mime = require('mime');
const fs = require('node:fs');
const { PassThrough } = require('node:stream');
const debug = require('debug')('url-inspector');
const { curly, CurlCode } = require('node-libcurl');
const { getProxyForUrl } = require('proxy-from-env');
const HttpError = require('http-errors');

const inspectSax = require('./sax');
const inspectEmbed = require('./embed');
const inspectStreat = require('./streat');

const archiveTypes = [
	"x-tar",
	"x-archive",
	"x-brotli",
	"x-bz2",
	"x-lzma",
	"x-lzip",
	"gzip",
	"x-xz",
	"x-compress",
	"zstd",
	"x-7z-compressed",
	"x-arj",
	"x-rar-compressed",
	"x-gtar",
	"zip"
];

// maximum bytes to download for each type of data
const inspectors = {
	embed: [inspectEmbed.embed, 30000],
	svg: [inspectSax.svg, 30000],
	image: [inspectStreat.media, 128000, 0.1],
	audio: [inspectStreat.media, 200000, 0.1],
	video: [inspectStreat.media, 512000, 0.1],
	html: [inspectSax.html, 512000],
	file: [inspectStreat.file, 32000],
	archive: [(obj, res, cb) => {
		cb(null, obj);
	}, 0]
};

exports.exists = async function (urlObj) {
	urlObj.method = 'HEAD';
	try {
		const req = await curlRequest(urlObj);
		const res = req.res;
		const status = res.statusCode;
		debug("remote", urlObj, "returns", status);
		if (status == 204 || res.headers['content-length'] == 0) {
			return false;
		} else if (status >= 200 && status < 400) {
			return parseType(res.headers['content-type']);
		} else {
			return false;
		}
	} catch (err) {
		return false;
	}
};

exports.request = async function (urlObj, obj) {
	debug("request url", urlObj.href);
	const req = await doRequest(urlObj);
	let res = req.res;

	const { statusCode, headers } = res;
	debug("got response status %d", statusCode);

	if (statusCode < 200 || (statusCode >= 400 && statusCode < 600)) {
		// definitely an error - status above 600 could be an anti-bot system
		throw new HttpError[statusCode]("Error fetching: " + urlObj.href);
	}
	if (headers.location) obj.location = new URL(headers.location, urlObj);
	let contentType = headers['content-type'];
	if (!contentType) contentType = mime.getType(Path.basename(urlObj.pathname));
	const mimeObj = parseType(contentType);
	obj.mime = MediaTyper.format(mimeObj);
	obj.what = mime2what(mimeObj);
	obj.ext = mime.getExtension(obj.mime);

	const contentLength = headers['content-length'];
	if (contentLength != null) {
		obj.size = parseInt(contentLength);
	}
	let disposition = headers['content-disposition'];
	if (disposition != null) {
		debug("got content disposition", disposition);
		if (disposition.startsWith('filename=')) disposition = 'attachment; ' + disposition;
		try {
			disposition = ContentDisposition.parse(disposition);
		} catch (ex) {
			debug("Unknown Content-Disposition format", ex);
		}
		if (disposition && disposition.parameters.filename) {
			urlObj = new URL(disposition.parameters.filename, urlObj);
		}
	}

	const fun = inspectors[getInspectorType(obj, mimeObj)];
	if (urlObj.protocol != "file:") pipeLimit(req, res, fun[1], fun[2]);

	debug("(mime, type, length) is (%s, %s, %d)", obj.mime, obj.what, obj.size);
	let charset = mimeObj.parameters && mimeObj.parameters.charset;
	if (charset) {
		charset = charset.toLowerCase();
		if (charset != "utf-8") {
			res = res.pipe(iconv.decodeStream(charset));
		} else {
			res.setEncoding(charset);
		}
	} else if (mimeObj.type == "text" || mimeObj.subtype == "svg" || mimeObj.suffix == "xml") {
		res.setEncoding("utf-8");
	}

	try {
		await fun[0](obj, res);
	} catch (err) {
		if (debug.enabled) throw err;
		else console.error(err);
	}
	req.abort();
	// request oembed when
	// - not blacklisted (noembed)
	// - has already or has found a oembed url
	// - does not have a thumbnail or does not have an html embed code,
	const fetchEmbed = !obj.noembed && obj.oembed && (!obj.thumbnail || !obj.html);
	delete obj.noembed;
	let canon = obj.canonical;
	if (canon && urlObj.protocol != "file:" && canon != obj.source && obj.nocanonical !== true) {
		canon = new URL(canon, urlObj);
		canon.redirects = (urlObj.redirects || 0) + 1;
	}
	delete obj.canonical;
	delete obj.nocanonical;

	try {
		if (fetchEmbed) {
			// prevent loops
			obj.noembed = true;
			obj.isEmbed = true;
			debug("fetch embed", obj.oembed);
			const urlObjEmbed = new URL(obj.oembed, urlObj);
			urlObjEmbed.headers = Object.assign({}, urlObj.headers);
			if (urlObj.protocol) urlObjEmbed.protocol = urlObj.protocol;
			const cobj = await exports.request(urlObjEmbed, Object.assign({}, obj));
			cobj.size = obj.size; // embed cannot tell the size
			return cobj;
		} else if (canon && canon.redirects < 5 && canon.pathname != urlObj.pathname && canon.pathname + '/' != urlObj.pathname && canon.pathname != urlObj.pathname + '/') {
			debug("fetch canonical url", canon.href);
			canon.headers = Object.assign({}, urlObj.headers);
			const cobj = await exports.request(canon, obj);
			cobj.location = canon;
			return cobj;
		} else {
			return obj;
		}
	} catch (err) {
		return obj;
	}
};

function getInspectorType({ isEmbed }, { type, subtype }) {
	let itype;
	if (subtype == "svg") itype = 'svg';
	else if (["image", "video", "audio"].includes(type)) itype = type;
	else if (/^x?html$/.test(subtype)) itype = 'html';
	else if (subtype == "json") itype = isEmbed ? 'embed' : 'archive';
	else if (archiveTypes.includes(subtype)) itype = 'archive';
	else itype = 'file';
	return itype;
}

async function doRequest(urlObj) {
	if (urlObj.protocol == "file:") {
		const stat = await fs.promises.stat(urlObj.pathname);
		const req = fs.createReadStream(urlObj.pathname);
		if (!req.abort) req.abort = req.destroy;
		req.headers = {
			'content-length': stat.size.toString()
		};
		req.local = true;
		req.res = req;
		return req;
	} else {
		try {
			const req = await curlRequest(urlObj);
			const res = req.res;
			if (res.statusCode == 403) {
				req.abort();
				throw new HttpError[res.statusCode]("Error fetching: " + urlObj.href);
			} else {
				return req;
			}
		} catch(err) {
			if (err.statusCode && urlObj.headers['User-Agent']) {
				debug("retrying with default curl ua");
				delete urlObj.headers["User-Agent"];
				return doRequest(urlObj);
			} else {
				throw err;
			}
		}
	}
}

function mime2what(obj) {
	let what = 'file';
	if (obj.subtype == "html") {
		what = 'page';
	} else if (obj.subtype == 'svg') {
		what = 'page';
	} else if (['image', 'audio', 'video'].includes(obj.type)) {
		what = obj.type;
	}
	return what;
}

function pipeLimit(req, res, length, percent) {
	if (!length) return req.abort();
	if (percent) {
		const contentLength = parseInt(res.headers['content-length']);
		if (!Number.isNaN(contentLength)) {
			length = Math.max(length, contentLength * percent);
		}
	}
	let curLength = 0;
	res.on('data', (buf) => {
		if (res.nolimit) return;
		curLength += buf.length;
		if (curLength >= length) {
			debug("got %d bytes, aborting", curLength);
			req.abort();
		}
	});
}

function parseType(str) {
	if (!str) return str;
	try {
		const ct = ContentType.parse(str);
		const mt = MediaTyper.parse(ct.type);
		return Object.assign(ct, mt);
	} catch (e) {
		return {};
	}
}

async function curlRequest(urlObj) {
	const headersList = Object.entries(urlObj.headers).map(([key, val]) => {
		return `${key}: ${val}`;
	});
	const method = (urlObj.method || "get").toLowerCase();
	const res = new PassThrough();

	const opts = {
		maxRedirs: 10,
		followLocation: true,
		acceptEncoding: "gzip, deflate, br",
		curlyLowerCaseHeaders: true,
		curlyStreamResponse: method == "get",
		curlyResponseBodyParsers: false,
		sslVerifyPeer: false,
		sslVerifyHost: false,
		httpHeader: headersList
	};
	const proxyUrl = getProxyForUrl(urlObj);
	if (proxyUrl) {
		if (proxyUrl.startsWith('https://')) {
			debug("https proxy", proxyUrl);
			opts.httpProxyTunnel = proxyUrl;
		} else {
			debug("http proxy", proxyUrl);
			opts.proxy = proxyUrl;
		}
	}

	const {
		headers: hlist,
		data,
		statusCode
	} = await curly[method](urlObj.href, opts);
	const req = {
		res,
		abort() {
			data.destroy();
		}
	};
	const headers = hlist.pop();
	delete headers.result;
	const last = hlist.pop();
	if (last && last.location) headers.location = last.location;
	res.headers = headers;
	res.statusCode = statusCode;
	if (data.on) data.on('error', err => {
		res.end();
		if (err.isCurlError && err.code === CurlCode.CURLE_ABORTED_BY_CALLBACK) {
			// this is expected
		} else {
			throw err;
		}
	});
	if (data.pipe) data.pipe(res);
	return req;
}

exports.get = curlRequest;
<commit_msg>Style and comment<commit_after>const Path = require('node:path');
const ContentDisposition = require('content-disposition');
const ContentType = require('content-type');
const MediaTyper = require('media-typer');
const iconv = require('iconv-lite');
const mime = require('mime');
const fs = require('node:fs');
const { PassThrough } = require('node:stream');
const debug = require('debug')('url-inspector');
const { curly, CurlCode } = require('node-libcurl');
const { getProxyForUrl } = require('proxy-from-env');
const HttpError = require('http-errors');

const inspectSax = require('./sax');
const inspectEmbed = require('./embed');
const inspectStreat = require('./streat');

const archiveTypes = [
	"x-tar",
	"x-archive",
	"x-brotli",
	"x-bz2",
	"x-lzma",
	"x-lzip",
	"gzip",
	"x-xz",
	"x-compress",
	"zstd",
	"x-7z-compressed",
	"x-arj",
	"x-rar-compressed",
	"x-gtar",
	"zip"
];

// maximum bytes to download for each type of data
const inspectors = {
	embed: [inspectEmbed.embed, 30000],
	svg: [inspectSax.svg, 30000],
	image: [inspectStreat.media, 128000, 0.1],
	audio: [inspectStreat.media, 200000, 0.1],
	video: [inspectStreat.media, 512000, 0.1],
	html: [inspectSax.html, 512000],
	file: [inspectStreat.file, 32000],
	archive: [(obj, res, cb) => {
		cb(null, obj);
	}, 0]
};

exports.exists = async function (urlObj) {
	urlObj.method = 'HEAD';
	try {
		const req = await curlRequest(urlObj);
		const res = req.res;
		const status = res.statusCode;
		debug("remote", urlObj, "returns", status);
		if (status == 204 || res.headers['content-length'] == 0) {
			return false;
		} else if (status >= 200 && status < 400) {
			return parseType(res.headers['content-type']);
		} else {
			return false;
		}
	} catch (err) {
		return false;
	}
};

exports.request = async function (urlObj, obj) {
	debug("request url", urlObj.href);
	const req = await doRequest(urlObj);
	let res = req.res;

	const { statusCode, headers } = res;
	debug("got response status %d", statusCode);

	if (statusCode < 200 || (statusCode >= 400 && statusCode < 600)) {
		// definitely an error - status above 600 could be an anti-bot system
		throw new HttpError[statusCode]("Error fetching: " + urlObj.href);
	}
	if (headers.location) obj.location = new URL(headers.location, urlObj);
	let contentType = headers['content-type'];
	if (!contentType) contentType = mime.getType(Path.basename(urlObj.pathname));
	const mimeObj = parseType(contentType);
	obj.mime = MediaTyper.format(mimeObj);
	obj.what = mime2what(mimeObj);
	obj.ext = mime.getExtension(obj.mime);

	const contentLength = headers['content-length'];
	if (contentLength != null) {
		obj.size = parseInt(contentLength);
	}
	let disposition = headers['content-disposition'];
	if (disposition != null) {
		debug("got content disposition", disposition);
		if (disposition.startsWith('filename=')) disposition = 'attachment; ' + disposition;
		try {
			disposition = ContentDisposition.parse(disposition);
		} catch (ex) {
			debug("Unknown Content-Disposition format", ex);
		}
		if (disposition && disposition.parameters.filename) {
			urlObj = new URL(disposition.parameters.filename, urlObj);
		}
	}

	const fun = inspectors[getInspectorType(obj, mimeObj)];
	if (urlObj.protocol != "file:") pipeLimit(req, res, fun[1], fun[2]);

	debug("(mime, type, length) is (%s, %s, %d)", obj.mime, obj.what, obj.size);
	let charset = mimeObj.parameters && mimeObj.parameters.charset;
	if (charset) {
		charset = charset.toLowerCase();
		if (charset != "utf-8") {
			res = res.pipe(iconv.decodeStream(charset));
		} else {
			res.setEncoding(charset);
		}
	} else if (mimeObj.type == "text" || mimeObj.subtype == "svg" || mimeObj.suffix == "xml") {
		res.setEncoding("utf-8");
	}

	try {
		await fun[0](obj, res);
	} catch (err) {
		if (debug.enabled) throw err;
		else console.error(err);
	}
	req.abort();
	// request oembed when
	// - not blacklisted (noembed)
	// - has already or has found a oembed url
	// - does not have a thumbnail or does not have an html embed code,
	const fetchEmbed = !obj.noembed && obj.oembed && (!obj.thumbnail || !obj.html);
	delete obj.noembed;
	let canon = obj.canonical;
	if (canon && urlObj.protocol != "file:" && canon != obj.source && obj.nocanonical !== true) {
		canon = new URL(canon, urlObj);
		canon.redirects = (urlObj.redirects || 0) + 1;
	}
	delete obj.canonical;
	delete obj.nocanonical;

	try {
		if (fetchEmbed) {
			// prevent loops
			obj.noembed = true;
			obj.isEmbed = true;
			debug("fetch embed", obj.oembed);
			const urlObjEmbed = new URL(obj.oembed, urlObj);
			urlObjEmbed.headers = Object.assign({}, urlObj.headers);
			if (urlObj.protocol) urlObjEmbed.protocol = urlObj.protocol;
			const cobj = await exports.request(urlObjEmbed, Object.assign({}, obj));
			cobj.size = obj.size; // embed cannot tell the size
			return cobj;
		} else if (canon && canon.redirects < 5 && canon.pathname != urlObj.pathname && canon.pathname + '/' != urlObj.pathname && canon.pathname != urlObj.pathname + '/') {
			debug("fetch canonical url", canon.href);
			canon.headers = Object.assign({}, urlObj.headers);
			const cobj = await exports.request(canon, obj);
			cobj.location = canon;
			return cobj;
		} else {
			return obj;
		}
	} catch (err) {
		return obj;
	}
};

function getInspectorType({ isEmbed }, { type, subtype }) {
	let itype;
	if (subtype == "svg") itype = 'svg';
	else if (["image", "video", "audio"].includes(type)) itype = type;
	else if (/^x?html$/.test(subtype)) itype = 'html';
	else if (subtype == "json") itype = isEmbed ? 'embed' : 'archive';
	else if (archiveTypes.includes(subtype)) itype = 'archive';
	else itype = 'file';
	return itype;
}

async function doRequest(urlObj) {
	if (urlObj.protocol == "file:") {
		const stat = await fs.promises.stat(urlObj.pathname);
		const req = fs.createReadStream(urlObj.pathname);
		if (!req.abort) req.abort = req.destroy;
		req.headers = {
			'content-length': stat.size.toString()
		};
		req.local = true;
		req.res = req;
		return req;
	} else {
		try {
			const req = await curlRequest(urlObj);
			const res = req.res;
			if (res.statusCode == 403) {
				req.abort();
				throw new HttpError[res.statusCode]("Error fetching: " + urlObj.href);
			} else {
				return req;
			}
		} catch(err) {
			if (err.statusCode && urlObj.headers['User-Agent']) {
				debug("retrying with default curl ua");
				delete urlObj.headers["User-Agent"];
				return doRequest(urlObj);
			} else {
				throw err;
			}
		}
	}
}

function mime2what(obj) {
	let what = 'file';
	if (obj.subtype == "html") {
		what = 'page';
	} else if (obj.subtype == 'svg') {
		what = 'page';
	} else if (['image', 'audio', 'video'].includes(obj.type)) {
		what = obj.type;
	}
	return what;
}

function pipeLimit(req, res, length, percent) {
	if (!length) return req.abort();
	if (percent) {
		const contentLength = parseInt(res.headers['content-length']);
		if (!Number.isNaN(contentLength)) {
			length = Math.max(length, contentLength * percent);
		}
	}
	let curLength = 0;
	res.on('data', buf => {
		if (res.nolimit) return;
		curLength += buf.length;
		if (curLength >= length) {
			debug("got %d bytes, aborting", curLength);
			req.abort();
		}
	});
}

function parseType(str) {
	if (!str) return str;
	try {
		const ct = ContentType.parse(str);
		const mt = MediaTyper.parse(ct.type);
		return Object.assign(ct, mt);
	} catch (e) {
		return {};
	}
}

async function curlRequest(urlObj) {
	const headersList = Object.entries(urlObj.headers).map(([key, val]) => {
		return `${key}: ${val}`;
	});
	const method = (urlObj.method || "get").toLowerCase();
	const res = new PassThrough();

	const opts = {
		maxRedirs: 10,
		followLocation: true,
		acceptEncoding: "gzip, deflate, br",
		curlyLowerCaseHeaders: true,
		curlyStreamResponse: method == "get",
		curlyResponseBodyParsers: false,
		sslVerifyPeer: false,
		sslVerifyHost: false,
		httpHeader: headersList
	};
	const proxyUrl = getProxyForUrl(urlObj);
	if (proxyUrl) {
		if (proxyUrl.startsWith('https://')) {
			debug("https proxy", proxyUrl);
			opts.httpProxyTunnel = proxyUrl;
		} else {
			debug("http proxy", proxyUrl);
			opts.proxy = proxyUrl;
		}
	}

	const {
		headers: hlist,
		data,
		statusCode
	} = await curly[method](urlObj.href, opts);
	const req = {
		res,
		abort() {
			data.destroy();
		}
	};
	const headers = hlist.pop();
	delete headers.result;
	const last = hlist.pop();
	if (last && last.location) headers.location = last.location;
	res.headers = headers;
	res.statusCode = statusCode;
	if (data.on) data.on('error', err => {
		res.end();
		if (err.isCurlError && err.code === CurlCode.CURLE_ABORTED_BY_CALLBACK) {
			// this is expected
		} else {
			// rethrow, let curly handle it
			throw err;
		}
	});
	if (data.pipe) data.pipe(res);
	return req;
}

exports.get = curlRequest;
<|endoftext|>


<commit_before>import moment from 'moment'

export default async (ctx, next, kitsu) => {
  console.log('anime: ' + ctx.request.body.text)
  await kitsu.searchAnime(encodeURI(ctx.request.body.text)).then(anime => {
    if (anime) {
      console.log(anime.canonicalTitle)
      let text = ''
      let fields = []
      let title_link = process.env.KITSU_HOST + '/anime/' + anime.slug
      let fallback = anime.canonicalTitle + ' - ' + title_link
      let { averageRating, showType, startDate, youtubeVideoId, ageRating, ageRatingGuide, nsfw } = anime

      if (anime.synopsis) {
        text = anime.synopsis
        fallback += `\n${anime.synopsis}`
      }

      if (averageRating) {
        averageRating = averageRating.toString().slice(0, 4)
        fields.push({
          title: ':bar_chart: Rating',
          value: averageRating,
          short: false
        })
        fallback += `\nRating: ${averageRating}`
      }

      if (showType) {
        showType = showType.charAt(0).toUpperCase() + showType.slice(1)
        fields.push({
          title: ':vhs: Type',
          value: showType,
          short: false
        })
        fallback += `\nType: ${showType}`
      }

      if (anime.episodeCount) {
        fields.push({
          title: ':clapper: Episodes',
          value: anime.episodeCount,
          short: false
        })
        fallback += `\nEpisodes: ${anime.episodeCount}`
      }

      if (anime.episodeLength) {
        fields.push({
          title: ':watch: Length',
          value: anime.episodeLength,
          short: false
        })
        fallback += `\nLength: ${anime.episodeLength} minutes`
      }

      if (startDate) {
        let date = moment(startDate, 'YYYY[-]MM[-]DD')
        startDate = `${date.format('MMMM Do YYYY')} (${date.fromNow()})`
        fields.push({
          title: ':spiral_calendar_pad: Date',
          value: startDate,
          short: false
        })
        fallback += `\nDate: ${startDate}`
      }

      if (ageRating) {
        if (ageRatingGuide)
          ageRating += ': ' + ageRatingGuide
        if (nsfw)
          ageRating += ' (NSFW)'
        fields.push({
          title: ':love_hotel: Age Rating',
          value: ageRating,
          short: false
        })
        fallback += `\nAge Rating: ${ageRating}`
      }

      if (youtubeVideoId) {
        youtubeVideoId = `https://www.youtube.com/watch?v=${youtubeVideoId}`
        fields.push({
          title: ':film_frames: Trailer',
          value: youtubeVideoId,
          short: false
        })
        fallback += `\nTrailer: ${youtubeVideoId}`
      }

      let actions = [
        {
          name: 'current',
          text: 'Currently Watching',
          style: 'primary',
          type: 'button'
        },
        {
          name: 'planned',
          text: 'Plan to Watch',
          type: 'button'
        },
        {
          name: 'completed',
          text: 'Completed',
          type: 'button'
        },
        {
          name: 'hold',
          text: 'On Hold',
          type: 'button'
        },
        {
          name: 'dropped',
          text: 'Dropped',
          type: 'button'
        }
      ]

      let body = {
        response_type: 'in_channel',
        link_names: true,
        attachments: [{
          color: '#F65440',
          mrkdwn_in: ['text'],
          callback_id: 'anime/' + anime.id,
          title: anime.canonicalTitle,
          title_link,
          text,
          image_url: anime.posterImage ? anime.posterImage.large : null,
          fields,
          fallback,
          // actions
        }]
      }

      ctx.status = 200
      ctx.body = body
    } else {
      ctx.status = 404
      throw new Error('Not Found')
    }
  })
}
<commit_msg>revert test<commit_after>import moment from 'moment'

export default async (ctx, next, kitsu) => {
  console.log('anime: ' + ctx.request.body.text)
  await kitsu.searchAnime(encodeURI(ctx.request.body.text)).then(anime => {
    if (anime) {
      console.log(anime.canonicalTitle)
      let text = ''
      let fields = []
      let title_link = process.env.KITSU_HOST + '/anime/' + anime.slug
      let fallback = anime.canonicalTitle + ' - ' + title_link
      let { averageRating, showType, startDate, youtubeVideoId, ageRating, ageRatingGuide, nsfw } = anime

      if (anime.synopsis) {
        text = anime.synopsis
        fallback += `\n${anime.synopsis}`
      }

      if (averageRating) {
        averageRating = averageRating.toString().slice(0, 4)
        fields.push({
          title: ':bar_chart: Rating',
          value: averageRating,
          short: true
        })
        fallback += `\nRating: ${averageRating}`
      }

      if (showType) {
        showType = showType.charAt(0).toUpperCase() + showType.slice(1)
        fields.push({
          title: ':vhs: Type',
          value: showType,
          short: true
        })
        fallback += `\nType: ${showType}`
      }

      if (anime.episodeCount) {
        fields.push({
          title: ':clapper: Episodes',
          value: anime.episodeCount,
          short: true
        })
        fallback += `\nEpisodes: ${anime.episodeCount}`
      }

      if (anime.episodeLength) {
        fields.push({
          title: ':watch: Length',
          value: anime.episodeLength,
          short: true
        })
        fallback += `\nLength: ${anime.episodeLength} minutes`
      }

      if (startDate) {
        let date = moment(startDate, 'YYYY[-]MM[-]DD')
        startDate = `${date.format('MMMM Do YYYY')} (${date.fromNow()})`
        fields.push({
          title: ':spiral_calendar_pad: Date',
          value: startDate,
          short: true
        })
        fallback += `\nDate: ${startDate}`
      }

      if (ageRating) {
        if (ageRatingGuide)
          ageRating += ': ' + ageRatingGuide
        if (nsfw)
          ageRating += ' (NSFW)'
        fields.push({
          title: ':love_hotel: Age Rating',
          value: ageRating,
          short: true
        })
        fallback += `\nAge Rating: ${ageRating}`
      }

      if (youtubeVideoId) {
        youtubeVideoId = `https://www.youtube.com/watch?v=${youtubeVideoId}`
        fields.push({
          title: ':film_frames: Trailer',
          value: youtubeVideoId,
          short: true
        })
        fallback += `\nTrailer: ${youtubeVideoId}`
      }

      let actions = [
        {
          name: 'current',
          text: 'Currently Watching',
          style: 'primary',
          type: 'button'
        },
        {
          name: 'planned',
          text: 'Plan to Watch',
          type: 'button'
        },
        {
          name: 'completed',
          text: 'Completed',
          type: 'button'
        },
        {
          name: 'hold',
          text: 'On Hold',
          type: 'button'
        },
        {
          name: 'dropped',
          text: 'Dropped',
          type: 'button'
        }
      ]

      let body = {
        response_type: 'in_channel',
        link_names: true,
        attachments: [{
          color: '#F65440',
          mrkdwn_in: ['text'],
          callback_id: 'anime/' + anime.id,
          title: anime.canonicalTitle,
          title_link,
          text,
          image_url: anime.posterImage ? anime.posterImage.large : null,
          fields,
          fallback,
          // actions
        }]
      }

      ctx.status = 200
      ctx.body = body
    } else {
      ctx.status = 404
      throw new Error('Not Found')
    }
  })
}
<|endoftext|>


<commit_before>/**
 * Created by john on 2017-01-11.
 */
define(function (require, exports, module) {

    var zhCN = require('datatableZh');
    var editorCN = require('i18n');

    exports.index = function ($, tableId, treeId, mapId, smapId) {

        //表格初始化
        var editor = new $.fn.dataTable.Editor({
            ajax: {
                create: {
                    type: 'POST',
                    url: '/admin/store',
                    data: {_token: $('meta[name="_token"]').attr('content')},
                },
                edit: {
                    type: 'PUT',
                    url: '/admin/store/_id_',
                    data: {_token: $('meta[name="_token"]').attr('content')},
                },
                remove: {
                    type: 'DELETE',
                    url: '/admin/store/_id_',
                    data: {_token: $('meta[name="_token"]').attr('content')},
                }
            },
            table: "#" + tableId,
            idSrc: 'id',
            i18n: editorCN,
            fields: [
                {'label': '门店全称', 'name': 'ffullname'},
                {'label': '门店简称', 'name': 'fshortname'},
                {'label': '客户详址', 'name': 'faddress'},
                {'label': '负责人', 'name': 'fcontracts'},
                {'label': '联系电话', 'name': 'ftelephone'},
                {'label': '渠道分类', 'name': 'fchannel'},
            ]
        });

        var table = $("#" + tableId).DataTable({
            dom: "lBfrtip",
            language: zhCN,
            processing: true,
            serverSide: true,
            select: true,
            paging: true,
            rowId: "id",
            searching: false,
            ajax: {
                url : '/admin/store/pagination',
            },
            columns: [
                {"data": "id"},
                {"data": "flongitude"},
                {"data": "flatitude"},
                {"data": "fnumber"},
                {"data": "ffullname"},
                // {"data": "fshortname"},
                {"data": "faddress"},
                {"data": "fcontracts"},
                {"data": "ftelephone"},
                {
                    "data": 'femp_id',
                    render: function (data, type, full) {
                        if (full.employee != null)
                            return full.employee.fname
                        else
                            return "";
                    }
                },
                {
                    "data": 'fcust_id',
                    render: function (data, type, full) {
                        if (full.customer != null)
                            return full.customer.fname
                        else
                            return "";
                    }
                },
                {
                    "data": 'fline_id',
                    render: function (data, type, full) {
                        if (full.line != null)
                            return full.line.fname
                        else
                            return "";
                    }
                },
                {
                    "data": 'fchannel',
                    render: function (data, type, full) {
                        if (full.channel != null)
                            return full.channel.fname
                        else
                            return "";
                    }
                },
                {
                    "data": "fis_signed",
                    render: function (data, type, full) {
                        if (data==0){
                            return '未签约';
                        }else {
                            return '已签约';
                        }
                    }
                },
                {
                    "data": 'id',
                    render: function (data, type, full) {
                        return '';
                    }
                },
            ],
            columnDefs: [
                {
                    "targets": [0,1,2],
                    "visible": false
                }
            ],
            buttons: [
                // { text: '新增', action: function () { }  },
                // { text: '编辑', className: 'edit', enabled: false },
                // { text: '删除', className: 'delete', enabled: false },
                {
                    text: '新增',
                    className: 'add',
                    enabled: false,
                    action: function () {

                        var femp_id = fempId(treeId,table);
                        if (femp_id==null){
                            layer.msg("请先选择一个业代！")
                            return false;
                        }else {
                            $("#storeInfoForm").attr('action','/admin/store/createStore')
                            $("#femp_id").val(femp_id)
                            $('#storeinfo').modal('show');
                        }

                    }
                },
                {
                    text: '编辑',
                    className: 'edit',
                    enabled: false,
                    action: function () {
                        var id = table.rows('.selected').data()[0].id;
                        $("#storeInfoForm").attr('action','/admin/store/editStore')
                        $("#store_id").val(id);
                        ajaxGetData("/admin/store/getStore/" + id,function (data) {
                            $("#storeInfoForm").find(".layui-input").each(function (index, element){
                                var name = $(element).attr('name');

                                var c = eval("data."+name);

                                $(element).val(c);

                                //地图标注
                                var point = new BMap.Point(data.flongitude, data.flatitude);
                                var marker = new BMap.Marker(point);  // 创建标注
                                smap.addOverlay(marker);
                                smap.panTo(point);
                            });

                            $("#storeInfoForm").find("select").each(function (index, element){
                                var name = $(element).attr('name');

                                var c = eval("data."+name);

                                $(element).val(c);
                                form.render('select')
                            });

                            $("#storeInfoForm").find('textarea').text(data.fremark);

                            //初始化门店图片
                            $('#storepic').fileinput('refresh', {
                                initialPreview: [ //预览图片的设置
                                    "",
                                ],
                            });

                            $('#storeinfo').modal('show');
                        });

                    }
                },

//                {extend: "create", text: '新增', editor: editor},
//                 {extend: "edit", text: '编辑', editor: editor},
                {extend: "remove", text: '删除', editor: editor},
                {extend: 'excel', text: '导出Excel'},
                {extend: 'print', text: '打印'},
            ]
        });

        //组织架构
        var getTreeData = function () {
            $.ajax({
                url: "../../admin/employee/employeeTree",
                type: "POST",
                data: {
                    '_token': $('meta[name="_token"]').attr('content'),
                    'dept_select': true
                },
                dataType: 'json',
                success: function (data) {
                    $("#" + treeId).treeview({
                        color: "#428bca",
                        enableLinks: true,
                        levels: 99,
                        data: data,
                        onNodeSelected: function (event, data) {
                            addEnable();
                            editEnable();
                            treeNodeSelect(treeId,table);
                        },
                        onNodeUnselected: function (event, data) {
                            editEnable();
                            addEnable();
                            treeNodeSelect(treeId,table);
                        },
                        onSearchComplete: function (event, data) {
                            if (JSON.stringify(data) != "{}") {

                            }
                        }
                    });
                },
            });
        }

        table.on('xhr', function () {
            map.clearOverlays();
            var data = table.ajax.json();
            for (var i = 0; i < data['data'].length; i++) {
                var st = data['data'][i];

                mapAddOverlay(st.flongitude, st.flatitude, st);
            }

        });

        table.on('select', rowselect).on( 'deselect', editEnable);

        //主表地图
        var map = new BMap.Map(mapId, {enableMapClick: false});

        //添加/编辑门店 地图选址
        var smap = new BMap.Map(smapId, {enableMapClick: false});

        //地图展示
        var params = {
            'zoom': 14,
        }
        mapInit(map,params);
        mapInit(smap,params);

        //地图点击事件
        var mapClick = function () {
            smap.addEventListener("click", function (e) {
                smap.clearOverlays();
                var point = new BMap.Point(e.point.lng, e.point.lat);
                var marker = new BMap.Marker(point);  // 创建标注
                smap.addOverlay(marker);

                map_address(point);

            });
        }

        var map_address = function (point) {
            var gc = new BMap.Geocoder();//地址解析类
            gc.getLocation(point, function(rs){
                $("#fprovince").val(rs.addressComponents.province);
                $("#fcity").val(rs.addressComponents.city);
                $("#fcountry").val(rs.addressComponents.district);
                $("#fstreet").val(rs.addressComponents.street);
                $("#flongitude").val(point.lng);
                $("#flatitude").val(point.lat);
            });
        }

        var mapAddOverlay = function (longitude, latitude, data) {
            var point = new BMap.Point(longitude, latitude);
            var marker = new BMap.Marker(point);  // 创建标注
            map.addOverlay(marker);              // 将标注添加到地图中
            map.panTo(point);

            if (data != null)
                infoWindow(marker, data);
        }

        //设置添加门店按钮是否可用
        var addEnable = function () {
            table.buttons(['.add']).enable(fempId(treeId,table)!=null);
        }

        //设置编辑门店按钮是否可用
        function editEnable() {
            var count = table.rows({selected: true}).count();
            table.buttons(['.edit']).enable(count > 0);
        }

        //modal关闭时数据清空
        $("#storeinfo").on("hidden.bs.modal", function () {
            smap.clearOverlays();
            $(".fileinput-remove-button").trigger('click')
            $("#storeInfoForm").find(".layui-input").val("");
        });

        //信息窗口
        function infoWindow(element, data) {

            var attrs = new Array();
            attrs.push({"name":"地址","value":data.faddress})
            attrs.push({"name":"负责人","value":data.fcontracts})
            attrs.push({"name":"电话","value":data.ftelephone})
            attrs.push({"name":"负责业代","value":data.employee.fname?data.employee.fname:''})
            attrs.push({"name":"所属客户","value":data.customer.fname?data.customer.fname:''})
            var obj = {"title":data.ffullname,"attrs":attrs};

            mapWindow(element,obj);
        }

        //表格单行选择事件 单点地图标注
        function rowselect() {
            addEnable();
            editEnable();

            map.clearOverlays();
            var data = table.rows('.selected').data()[0];

            mapAddOverlay(data.flongitude, data.flatitude, data);
        }

        layui.use(['layer', 'form'], function () {
             layer = layui.layer
                , form = layui.form();

            form.verify({
                map: function(){
                    if($("#flongitude").val()==""||$("#flatitude").val()==""){
                        return '请在地图中标注出门店的位置！';
                    }
                }
            })

            form.on('submit(storeInfoForm)', function (data) {
                ajaxForm("#storeInfoForm",function () {
                    $("#storeinfo").modal('hide');
                    table.ajax.reload();
                });
                return false;
            });

        });

        mapClick();
        getTreeData();

        /*
         *   地图关键字搜索
         */
        function G(id) {
            return document.getElementById(id);
        }

        var ac = new BMap.Autocomplete(    //建立一个自动完成的对象
            {"input" : "suggestId"
                ,"location" : smap
            });

        ac.addEventListener("onhighlight", function(e) {  //鼠标放在下拉列表上的事件
            var str = "";
            var _value = e.fromitem.value;
            var value = "";
            if (e.fromitem.index > -1) {
                value = _value.province +  _value.city +  _value.district +  _value.street +  _value.business;
            }
            str = "FromItem
index = " + e.fromitem.index + "
value = " + value;

            value = "";
            if (e.toitem.index > -1) {
                _value = e.toitem.value;
                value = _value.province +  _value.city +  _value.district +  _value.street +  _value.business;
            }
            str += "
ToItem
index = " + e.toitem.index + "
value = " + value;
            G("searchResultPanel").innerHTML = str;
        });

        var myValue;
        ac.addEventListener("onconfirm", function(e) {    //鼠标点击下拉列表后的事件
            var _value = e.item.value;
            myValue = _value.province +  _value.city +  _value.district +  _value.street +  _value.business;
            G("searchResultPanel").innerHTML ="onconfirm
index = " + e.item.index + "
myValue = " + myValue;

            setPlace();
        });

        function setPlace(){
            smap.clearOverlays();    //清除地图上所有覆盖物
            function myFun(){
                var point = local.getResults().getPoi(0).point;    //获取第一个智能搜索的结果
                smap.centerAndZoom(point, 18);
                // smap.addOverlay(new BMap.Marker(point));    //添加标注
                //
                // console.log(point);
                map_address(point);

                var marker = new BMap.Marker(point);  // 创建标注
                smap.addOverlay(marker);
            }
            var local = new BMap.LocalSearch(smap, { //智能搜索
                onSearchComplete: myFun
            });
            local.search(myValue);
        }

        /*
         *   地图关键字搜索 end!
         */

    }

});<commit_msg>bug fix<commit_after>/**
 * Created by john on 2017-01-11.
 */
define(function (require, exports, module) {

    var zhCN = require('datatableZh');
    var editorCN = require('i18n');

    exports.index = function ($, tableId, treeId, mapId, smapId) {

        //表格初始化
        var editor = new $.fn.dataTable.Editor({
            ajax: {
                create: {
                    type: 'POST',
                    url: '/admin/store',
                    data: {_token: $('meta[name="_token"]').attr('content')},
                },
                edit: {
                    type: 'PUT',
                    url: '/admin/store/_id_',
                    data: {_token: $('meta[name="_token"]').attr('content')},
                },
                remove: {
                    type: 'DELETE',
                    url: '/admin/store/_id_',
                    data: {_token: $('meta[name="_token"]').attr('content')},
                }
            },
            table: "#" + tableId,
            idSrc: 'id',
            i18n: editorCN,
            fields: [
                {'label': '门店全称', 'name': 'ffullname'},
                {'label': '门店简称', 'name': 'fshortname'},
                {'label': '客户详址', 'name': 'faddress'},
                {'label': '负责人', 'name': 'fcontracts'},
                {'label': '联系电话', 'name': 'ftelephone'},
                {'label': '渠道分类', 'name': 'fchannel'},
            ]
        });

        var table = $("#" + tableId).DataTable({
            dom: "lBfrtip",
            language: zhCN,
            processing: true,
            serverSide: true,
            select: true,
            paging: true,
            rowId: "id",
            searching: false,
            ajax: {
                url : '/admin/store/pagination',
            },
            columns: [
                {"data": "id"},
                {"data": "flongitude"},
                {"data": "flatitude"},
                {"data": "fnumber"},
                {"data": "ffullname"},
                // {"data": "fshortname"},
                {"data": "faddress"},
                {"data": "fcontracts"},
                {"data": "ftelephone"},
                {
                    "data": 'femp_id',
                    render: function (data, type, full) {
                        if (full.employee != null)
                            return full.employee.fname
                        else
                            return "";
                    }
                },
                {
                    "data": 'fcust_id',
                    render: function (data, type, full) {
                        if (full.customer != null)
                            return full.customer.fname
                        else
                            return "";
                    }
                },
                {
                    "data": 'fline_id',
                    render: function (data, type, full) {
                        if (full.line != null)
                            return full.line.fname
                        else
                            return "";
                    }
                },
                {
                    "data": 'fchannel',
                    render: function (data, type, full) {
                        if (full.channel != null)
                            return full.channel.fname
                        else
                            return "";
                    }
                },
                {
                    "data": "fis_signed",
                    render: function (data, type, full) {
                        if (data==0){
                            return '未签约';
                        }else {
                            return '已签约';
                        }
                    }
                },
                {
                    "data": 'id',
                    render: function (data, type, full) {
                        return '';
                    }
                },
            ],
            columnDefs: [
                {
                    "targets": [0,1,2],
                    "visible": false
                }
            ],
            buttons: [
                // { text: '新增', action: function () { }  },
                // { text: '编辑', className: 'edit', enabled: false },
                // { text: '删除', className: 'delete', enabled: false },
                {
                    text: '新增',
                    className: 'add',
                    enabled: false,
                    action: function () {

                        var femp_id = fempId(treeId,table);
                        if (femp_id==null){
                            layer.msg("请先选择一个业代！")
                            return false;
                        }else {
                            $("#storeInfoForm").attr('action','/admin/store/createStore')
                            $("#femp_id").val(femp_id)
                            $('#storeinfo').modal('show');
                        }

                    }
                },
                {
                    text: '编辑',
                    className: 'edit',
                    enabled: false,
                    action: function () {
                        var id = table.rows('.selected').data()[0].id;
                        $("#storeInfoForm").attr('action','/admin/store/editStore')
                        $("#store_id").val(id);
                        ajaxGetData("/admin/store/getStore/" + id,function (data) {
                            $("#storeInfoForm").find(".layui-input").each(function (index, element){
                                var name = $(element).attr('name');

                                var c = eval("data."+name);

                                $(element).val(c);

                                //地图标注
                                var point = new BMap.Point(data.flongitude, data.flatitude);
                                var marker = new BMap.Marker(point);  // 创建标注
                                smap.addOverlay(marker);
                                smap.panTo(point);
                            });

                            $("#storeInfoForm").find("select").each(function (index, element){
                                var name = $(element).attr('name');

                                var c = eval("data."+name);

                                $(element).val(c);
                                form.render('select')
                            });

                            $("#storeInfoForm").find('textarea').text(data.fremark);

                            //初始化门店图片
                            $('#storepic').fileinput('refresh', {
                                initialPreview: [ //预览图片的设置
                                    "",
                                ],
                            });

                            $('#storeinfo').modal('show');
                        });

                    }
                },

//                {extend: "create", text: '新增', editor: editor},
//                 {extend: "edit", text: '编辑', editor: editor},
                {extend: "remove", text: '删除', editor: editor},
                {extend: 'excel', text: '导出Excel'},
                {extend: 'print', text: '打印'},
            ]
        });

        //组织架构
        var getTreeData = function () {
            $.ajax({
                url: "../../admin/employee/employeeTree",
                type: "POST",
                data: {
                    '_token': $('meta[name="_token"]').attr('content'),
                    'dept_select': true
                },
                dataType: 'json',
                success: function (data) {
                    $("#" + treeId).treeview({
                        color: "#428bca",
                        enableLinks: true,
                        levels: 99,
                        data: data,
                        onNodeSelected: function (event, data) {
                            addEnable();
                            editEnable();
                            treeNodeSelect(treeId,table);
                        },
                        onNodeUnselected: function (event, data) {
                            editEnable();
                            addEnable();
                            treeNodeSelect(treeId,table);
                        },
                        onSearchComplete: function (event, data) {
                            if (JSON.stringify(data) != "{}") {

                            }
                        }
                    });
                },
            });
        }

        table.on('xhr', function () {
            map.clearOverlays();
            var data = table.ajax.json();
            for (var i = 0; i < data['data'].length; i++) {
                var st = data['data'][i];

                mapAddOverlay(st.flongitude, st.flatitude, st);
            }

        });

        table.on('select', rowselect).on( 'deselect', editEnable);

        //主表地图
        var map = new BMap.Map(mapId, {enableMapClick: false});

        //添加/编辑门店 地图选址
        var smap = new BMap.Map(smapId, {enableMapClick: false});

        //地图展示
        var params = {
            'zoom': 14,
        }
        mapInit(map,params);
        mapInit(smap,params);

        //地图点击事件
        var mapClick = function () {
            smap.addEventListener("click", function (e) {
                smap.clearOverlays();
                var point = new BMap.Point(e.point.lng, e.point.lat);
                var marker = new BMap.Marker(point);  // 创建标注
                smap.addOverlay(marker);

                map_address(point);

            });
        }

        var map_address = function (point) {
            var gc = new BMap.Geocoder();//地址解析类
            gc.getLocation(point, function(rs){
                $("#fprovince").val(rs.addressComponents.province);
                $("#fcity").val(rs.addressComponents.city);
                $("#fcountry").val(rs.addressComponents.district);
                $("#fstreet").val(rs.addressComponents.street);
                $("#flongitude").val(point.lng);
                $("#flatitude").val(point.lat);
            });
        }

        var mapAddOverlay = function (longitude, latitude, data) {
            var point = new BMap.Point(longitude, latitude);
            var marker = new BMap.Marker(point);  // 创建标注
            map.addOverlay(marker);              // 将标注添加到地图中
            map.panTo(point);

            if (data != null)
                infoWindow(marker, data);
        }

        //设置添加门店按钮是否可用
        var addEnable = function () {
            table.buttons(['.add']).enable(fempId(treeId,table)!=null);
        }

        //设置编辑门店按钮是否可用
        function editEnable() {
            var count = table.rows({selected: true}).count();
            table.buttons(['.edit']).enable(count > 0);
        }

        //modal关闭时数据清空
        $("#storeinfo").on("hidden.bs.modal", function () {
            smap.clearOverlays();
            $(".fileinput-remove-button").trigger('click')
            $("#storeInfoForm").find(".layui-input").val("");
        });

        //信息窗口
        function infoWindow(element, data) {

            var attrs = new Array();
            attrs.push({"name":"地址","value":data.faddress})
            attrs.push({"name":"负责人","value":data.fcontracts})
            attrs.push({"name":"电话","value":data.ftelephone})
            attrs.push({"name":"负责业代","value":data.employee.fname?data.employee.fname:''})
            if(data.customer.fname)
                attrs.push({"name":"所属客户","value":data.customer.fname})
            var obj = {"title":data.ffullname,"attrs":attrs};

            mapWindow(element,obj);
        }

        //表格单行选择事件 单点地图标注
        function rowselect() {
            addEnable();
            editEnable();

            map.clearOverlays();
            var data = table.rows('.selected').data()[0];

            mapAddOverlay(data.flongitude, data.flatitude, data);
        }

        layui.use(['layer', 'form'], function () {
             layer = layui.layer
                , form = layui.form();

            form.verify({
                map: function(){
                    if($("#flongitude").val()==""||$("#flatitude").val()==""){
                        return '请在地图中标注出门店的位置！';
                    }
                }
            })

            form.on('submit(storeInfoForm)', function (data) {
                ajaxForm("#storeInfoForm",function () {
                    $("#storeinfo").modal('hide');
                    table.ajax.reload();
                });
                return false;
            });

        });

        mapClick();
        getTreeData();

        /*
         *   地图关键字搜索
         */
        function G(id) {
            return document.getElementById(id);
        }

        var ac = new BMap.Autocomplete(    //建立一个自动完成的对象
            {"input" : "suggestId"
                ,"location" : smap
            });

        ac.addEventListener("onhighlight", function(e) {  //鼠标放在下拉列表上的事件
            var str = "";
            var _value = e.fromitem.value;
            var value = "";
            if (e.fromitem.index > -1) {
                value = _value.province +  _value.city +  _value.district +  _value.street +  _value.business;
            }
            str = "FromItem
index = " + e.fromitem.index + "
value = " + value;

            value = "";
            if (e.toitem.index > -1) {
                _value = e.toitem.value;
                value = _value.province +  _value.city +  _value.district +  _value.street +  _value.business;
            }
            str += "
ToItem
index = " + e.toitem.index + "
value = " + value;
            G("searchResultPanel").innerHTML = str;
        });

        var myValue;
        ac.addEventListener("onconfirm", function(e) {    //鼠标点击下拉列表后的事件
            var _value = e.item.value;
            myValue = _value.province +  _value.city +  _value.district +  _value.street +  _value.business;
            G("searchResultPanel").innerHTML ="onconfirm
index = " + e.item.index + "
myValue = " + myValue;

            setPlace();
        });

        function setPlace(){
            smap.clearOverlays();    //清除地图上所有覆盖物
            function myFun(){
                var point = local.getResults().getPoi(0).point;    //获取第一个智能搜索的结果
                smap.centerAndZoom(point, 18);
                // smap.addOverlay(new BMap.Marker(point));    //添加标注
                //
                // console.log(point);
                map_address(point);

                var marker = new BMap.Marker(point);  // 创建标注
                smap.addOverlay(marker);
            }
            var local = new BMap.LocalSearch(smap, { //智能搜索
                onSearchComplete: myFun
            });
            local.search(myValue);
        }

        /*
         *   地图关键字搜索 end!
         */

    }

});<|endoftext|>


<commit_before>/* eslint-disable no-underscore-dangle */

import { Component, PropTypes, createElement } from 'react';
import omit from 'zent-utils/lodash/omit';
import find from 'zent-utils/lodash/find';
import noop from 'zent-utils/lodash/noop';
import assign from 'zent-utils/lodash/assign';
import isArray from 'zent-utils/lodash/isArray';
import isEqual from 'zent-utils/lodash/isEqual';
import some from 'zent-utils/lodash/some';
import isPromise from 'zent-utils/isPromise';
import { getDisplayName, silenceEvent, silenceEvents } from './utils';
import rules from './validationRules';
import handleSubmit from './handleSubmit';

const emptyArray = [];
const checkSubmit = submit => {
  if (!submit || typeof submit !== 'function') {
    throw new Error('You must either pass handleSubmit() an onSubmit function or pass onSubmit as a prop');
  }
  return submit;
};
const createForm = (config = {}) => {
  const { formValidations } = config;
  const validationRules = assign({}, rules, formValidations);

  return WrappedComponent => {
    return class Form extends Component {
      constructor(props) {
        super(props);
        this.state = {
          isFormValid: true,
          isSubmitting: false,
        };
        this.fields = [];
        this._isMounted = false;
      }

      static displayName = `Form(${getDisplayName(WrappedComponent)})`
      static WrappedComponent = WrappedComponent

      static propTypes = {
        onSubmit: PropTypes.func,
        onValid: PropTypes.func,
        onInvalid: PropTypes.func,
        onChange: PropTypes.func,
        validationErrors: PropTypes.object
      }

      static defaultProps = {
        onSubmit: noop,
        onSubmitSuccess: noop,
        onSubmitFail: noop,
        onValid: noop,
        onInvalid: noop,
        onChange: noop,
        validationErrors: null
      }

      static childContextTypes = {
        zentForm: PropTypes.object
      }

      getChildContext() {
        return {
          zentForm: {
            attachToForm: this.attachToForm,
            detachFromForm: this.detachFromForm,
            validate: this.validate,
            asyncValidate: this.asyncValidate,
            getFormValues: this.getFormValues,
            getFieldError: this.getFieldError,
            isValidValue: this.isValidValue,
            setFieldExternalErrors: this.setFieldExternalErrors,
            resetFieldsValue: this.resetFieldsValue,
            setFormPristine: this.setFormPristine,
            isValid: this.isValid,
            isSubmitting: this.isSubmitting
          }
        };
      }

      componentDidMount() {
        this.validateForm();
        this._isMounted = true;
      }

      componentWillUpdate() {
        this.prevFieldNames = this.fields.map(field => field.props.name);
      }

      componentDidUpdate() {
        const { validationErrors } = this.props;
        if (validationErrors &&
            typeof validationErrors === 'object' &&
            Object.keys(validationErrors).length > 0) {
          this.setFieldValidationErrors(validationErrors);
        }

        const newFieldNames = this.fields.map(field => field.props.name);
        if (!isEqual(this.prevFieldNames, newFieldNames)) {
          this.validateForm();
        }
      }

      submitCompleted = (result) => {
        delete this.submitPromise;
        return result;
      }

      submitFailed = (error) => {
        delete this.submitPromise;
        throw error;
      }

      listenToSubmit = (promise) => {
        if (!isPromise(promise)) {
          return promise;
        }
        // 当submit是一个promise时，需要一个标识表明正在提交，提交结束后删除标识
        this.submitPromise = promise;
        return promise.then(this.submitCompleted, this.submitFailed);
      }

      submit = (submitOrEvent) => {
        const { onSubmit } = this.props;

        // 在表单中手动调用handleSubmit或者把handleSubmit赋值给表单的onSubmit回调
        // handleSubmit赋值给表单的onSubmit时，submitOrEvent是一个event对象
        // handleSubmit的参数必须是function
        if (!submitOrEvent || silenceEvent(submitOrEvent)) {
          if (!this.submitPromise) {
            // 调用props传入的onSubmit方法
            return this.listenToSubmit(handleSubmit(checkSubmit(onSubmit), this));
          }
        } else {
          // submitOrEvent是一个自定义submit函数，返回一个promise对象
          return silenceEvents(() =>
            !this.submitPromise &&
            this.listenToSubmit(handleSubmit(checkSubmit(submitOrEvent), this)));
        }
      }

      isSubmitting = () => {
        return this.state.isSubmitting;
      }

      isValid = () => {
        return this.state.isFormValid;
      }

      setFieldValidationErrors = (errors) => {
        this.fields.forEach(field => {
          const name = field.props.name;
          field.setState({
            _isValid: !(name in errors),
            _validationError: typeof errors[name] === 'string' ? [errors[name]] : errors[name]
          });
        });
      }

      // 设置服务端返回的错误信息
      setFieldExternalErrors = (errors) => {
        Object.keys(errors).forEach((name) => {
          const field = find(this.fields, (component) => component.props.name === name);
          if (!field) {
            throw new Error(`field ${name} does not exits`);
          }
          field.setState({
            _isValid: false,
            _externalError: typeof errors[name] === 'string' ?
              [errors[name]] :
              errors[name]
          });
        });
      }

      setFormPristine = (isPristine) => {
        this.fields.forEach(field => {
          field.setState({
            _isPristine: isPristine
          });
        });
      }

      resetFieldsValue= (data) => {
        this.fields.forEach(field => {
          const name = field.props.name;
          if (data && data.hasOwnProperty(name)) {
            field.setValue(data[name]);
          } else {
            field.resetValue();
          }
        });
      }

      reset = (data) => {
        this.setFormPristine(true);
        this.resetFieldsValue(data);
      }

      isFieldTouched = (name) => {
        const field = this.fields.find(component => component.props.name === name);

        if (!field) return false;
        return !field.isPristine();
      }

      isFieldValidating = (name) => {
        const field = this.fields.find(component => component.props.name === name);

        if (!field) return false;
        return !field.isValidating();
      }

      getFieldError = (name) => {
        const field = this.fields.find(component => component.props.name === name);

        if (!field) return '';
        return field.getErrorMessage();
      }

      getFormValues = () => {
        return this.fields.reduce((values, field) => {
          const name = field.props.name;
          values[name] = field.getValue();
          return values;
        }, {});
      }

      getValidationErrors = () => {
        return this.fields.reduce((errors, field) => {
          const name = field.props.name;
          errors[name] = field.getErrorMessage();
          return errors;
        }, {});
      }

      getPristineValues = () => {
        return this.fields.reduce((values, field) => {
          const name = field.props.name;
          values[name] = field.getPristineValue();
          return values;
        }, {});
      }

      isChanged = () => {
        return !isEqual(this.getPristineValues(), this.getFormValues());
      }

      isValidating = () => {
        return some(this.fields, (field) => {
          return field.isValidating();
        });
      }

      isValidValue = (field, value) => {
        return this.runValidation(field, value).isValid;
      }

      runValidation = (field, value = field.getValue()) => {
        const formValidationErrors = this.props.validationErrors;
        const { name, validationError, validationErrors } = field.props;
        const currentValues = this.getFormValues();

        const validationResults = this.runRules(value, currentValues, field._validations);
        const isValid = !validationResults.failed.length &&
          !(formValidationErrors && formValidationErrors[field.props.name]);

        return {
          isValid,
          error: (function () {
            if (isValid) {
              return emptyArray;
            }

            if (validationResults.errors.length) {
              return validationResults.errors;
            }

            if (formValidationErrors && formValidationErrors[name]) {
              return typeof formValidationErrors[name] === 'string' ?
                [formValidationErrors[name]] :
                formValidationErrors[name];
            }

            if (validationResults.failed.length) {
              return validationResults.failed.map((failed) => {
                return validationErrors[failed] ?
                  validationErrors[failed] :
                  validationError;
              }).filter((x, pos, arr) => {
                // Remove duplicates
                return arr.indexOf(x) === pos;
              });
            }
          }())
        };
      }

      runRules = (value, currentValues, validations = []) => {
        const results = {
          errors: [],
          failed: []
        };

        function updateResults(validation, validationMethod) {
          // validation方法可以直接返回错误信息，否则需要返回布尔值表明校验是否成功
          if (typeof validation === 'string') {
            results.errors.push(validation);
            results.failed.push(validationMethod);
          } else if (!validation) {
            results.failed.push(validationMethod);
          }
        }

        Object.keys(validations).forEach((validationMethod) => {
          // validations中不指定function则必须是内置的rule
          if (!validationRules[validationMethod] && typeof validations[validationMethod] !== 'function') {
            throw new Error(`does not have the validation rule: ${validationMethod}`);
          }

          // 使用自定义校验方法或内置校验方法（可以按需添加）
          if (typeof validations[validationMethod] === 'function') {
            const validation = validations[validationMethod](currentValues, value);
            updateResults(validation, validationMethod);
          } else if (typeof validations[validationMethod] !== 'function') {
            const validation = validationRules[validationMethod](currentValues, value, validations[validationMethod]);
            updateResults(validation, validationMethod);
          }
        });

        return results;
      }

      validate = (field) => {
        // 初始化时调用validate不触发onChange
        if (this._isMounted) {
          this.props.onChange(this.getFormValues(), this.isChanged());
        }

        const validation = this.runValidation(field);

        field.setState({
          _isValid: validation.isValid,
          _validationError: validation.error,
          _externalError: null
        }, this.validateForm);
      }

      asyncValidate = (field, value) => {
        const { asyncValidation } = field.props;
        const values = this.getFormValues();

        if (!field.isValid()) return;

        field.setState({
          _isValidating: true
        });

        const promise = asyncValidation(values, value);
        if (!isPromise(promise)) {
          throw new Error('asyncValidation function must return a promise');
        }

        const handleResult = rejected => error => {
          field.setState({
            _isValidating: false,
            _isValid: !rejected,
            _asyncValidationError: error ? [error] : null
          });
        };

        return promise.then(handleResult(false), handleResult(true));
      }

      validateForm = () => {
        const onValidationComplete = () => {
          const allIsValid = this.fields.every(field => {
            return field.isValid();
          });

          this.setState({
            isFormValid: allIsValid
          });

          if (allIsValid) {
            this.props.onValid();
          } else {
            this.props.onInvalid();
          }
        };

        this.fields.forEach((field, index) => {
          const { _externalError, _asyncValidationError } = field.state;
          const validation = this.runValidation(field);
          if (validation.isValid && (_externalError || _asyncValidationError)) {
            validation.isValid = false;
          }

          field.setState({
            _isValid: validation.isValid,
            _validationError: validation.error,
            _externalError: !validation.isValid && _externalError ?
              _externalError :
              null
          }, index === this.fields.length - 1 ? onValidationComplete : null);
        });
      }

      attachToForm = (field) => {
        if (this.fields.indexOf(field) < 0) {
          this.fields.push(field);
        }
        // form初始化时不校验，后续动态添加的元素再校验
        this._isMounted && this.validate(field);
      }

      detachFromForm = (field) => {
        const fieldPos = this.fields.indexOf(field);
        if (fieldPos >= 0) {
          this.fields.splice(fieldPos, 1);
        }
        this.validateForm();
      }

      render() {
        const passableProps = omit(this.props, [
          'validationErrors',
          'handleSubmit',
          'onChange'
        ]);

        return createElement(WrappedComponent, {
          ...passableProps,
          handleSubmit: this.submit,
          zentForm: {
            getFormValues: this.getFormValues,
            getFieldError: this.getFieldError,
            setFieldExternalErrors: this.setFieldExternalErrors,
            resetFieldsValue: this.resetFieldsValue,
            setFormPristine: this.setFormPristine,
            isFieldTouched: this.isFieldTouched,
            isFieldValidating: this.isFieldValidating,
            isValid: this.isValid,
            isSubmitting: this.isSubmitting
          }
        });
      }
    };
  };
};

export default createForm;
<commit_msg>fix eslint error<commit_after>/* eslint-disable no-underscore-dangle */

import { Component, PropTypes, createElement } from 'react';
import omit from 'zent-utils/lodash/omit';
import find from 'zent-utils/lodash/find';
import noop from 'zent-utils/lodash/noop';
import assign from 'zent-utils/lodash/assign';
import isEqual from 'zent-utils/lodash/isEqual';
import some from 'zent-utils/lodash/some';
import isPromise from 'zent-utils/isPromise';
import { getDisplayName, silenceEvent, silenceEvents } from './utils';
import rules from './validationRules';
import handleSubmit from './handleSubmit';

const emptyArray = [];
const checkSubmit = submit => {
  if (!submit || typeof submit !== 'function') {
    throw new Error('You must either pass handleSubmit() an onSubmit function or pass onSubmit as a prop');
  }
  return submit;
};
const createForm = (config = {}) => {
  const { formValidations } = config;
  const validationRules = assign({}, rules, formValidations);

  return WrappedComponent => {
    return class Form extends Component {
      constructor(props) {
        super(props);
        this.state = {
          isFormValid: true,
          isSubmitting: false,
        };
        this.fields = [];
        this._isMounted = false;
      }

      static displayName = `Form(${getDisplayName(WrappedComponent)})`
      static WrappedComponent = WrappedComponent

      static propTypes = {
        onSubmit: PropTypes.func,
        onValid: PropTypes.func,
        onInvalid: PropTypes.func,
        onChange: PropTypes.func,
        validationErrors: PropTypes.object
      }

      static defaultProps = {
        onSubmit: noop,
        onSubmitSuccess: noop,
        onSubmitFail: noop,
        onValid: noop,
        onInvalid: noop,
        onChange: noop,
        validationErrors: null
      }

      static childContextTypes = {
        zentForm: PropTypes.object
      }

      getChildContext() {
        return {
          zentForm: {
            attachToForm: this.attachToForm,
            detachFromForm: this.detachFromForm,
            validate: this.validate,
            asyncValidate: this.asyncValidate,
            getFormValues: this.getFormValues,
            getFieldError: this.getFieldError,
            isValidValue: this.isValidValue,
            setFieldExternalErrors: this.setFieldExternalErrors,
            resetFieldsValue: this.resetFieldsValue,
            setFormPristine: this.setFormPristine,
            isValid: this.isValid,
            isSubmitting: this.isSubmitting
          }
        };
      }

      componentDidMount() {
        this.validateForm();
        this._isMounted = true;
      }

      componentWillUpdate() {
        this.prevFieldNames = this.fields.map(field => field.props.name);
      }

      componentDidUpdate() {
        const { validationErrors } = this.props;
        if (validationErrors &&
            typeof validationErrors === 'object' &&
            Object.keys(validationErrors).length > 0) {
          this.setFieldValidationErrors(validationErrors);
        }

        const newFieldNames = this.fields.map(field => field.props.name);
        if (!isEqual(this.prevFieldNames, newFieldNames)) {
          this.validateForm();
        }
      }

      submitCompleted = (result) => {
        delete this.submitPromise;
        return result;
      }

      submitFailed = (error) => {
        delete this.submitPromise;
        throw error;
      }

      listenToSubmit = (promise) => {
        if (!isPromise(promise)) {
          return promise;
        }
        // 当submit是一个promise时，需要一个标识表明正在提交，提交结束后删除标识
        this.submitPromise = promise;
        return promise.then(this.submitCompleted, this.submitFailed);
      }

      submit = (submitOrEvent) => {
        const { onSubmit } = this.props;

        // 在表单中手动调用handleSubmit或者把handleSubmit赋值给表单的onSubmit回调
        // handleSubmit赋值给表单的onSubmit时，submitOrEvent是一个event对象
        // handleSubmit的参数必须是function
        if (!submitOrEvent || silenceEvent(submitOrEvent)) {
          if (!this.submitPromise) {
            // 调用props传入的onSubmit方法
            return this.listenToSubmit(handleSubmit(checkSubmit(onSubmit), this));
          }
        } else {
          // submitOrEvent是一个自定义submit函数，返回一个promise对象
          return silenceEvents(() =>
            !this.submitPromise &&
            this.listenToSubmit(handleSubmit(checkSubmit(submitOrEvent), this)));
        }
      }

      isSubmitting = () => {
        return this.state.isSubmitting;
      }

      isValid = () => {
        return this.state.isFormValid;
      }

      setFieldValidationErrors = (errors) => {
        this.fields.forEach(field => {
          const name = field.props.name;
          field.setState({
            _isValid: !(name in errors),
            _validationError: typeof errors[name] === 'string' ? [errors[name]] : errors[name]
          });
        });
      }

      // 设置服务端返回的错误信息
      setFieldExternalErrors = (errors) => {
        Object.keys(errors).forEach((name) => {
          const field = find(this.fields, (component) => component.props.name === name);
          if (!field) {
            throw new Error(`field ${name} does not exits`);
          }
          field.setState({
            _isValid: false,
            _externalError: typeof errors[name] === 'string' ?
              [errors[name]] :
              errors[name]
          });
        });
      }

      setFormPristine = (isPristine) => {
        this.fields.forEach(field => {
          field.setState({
            _isPristine: isPristine
          });
        });
      }

      resetFieldsValue= (data) => {
        this.fields.forEach(field => {
          const name = field.props.name;
          if (data && data.hasOwnProperty(name)) {
            field.setValue(data[name]);
          } else {
            field.resetValue();
          }
        });
      }

      reset = (data) => {
        this.setFormPristine(true);
        this.resetFieldsValue(data);
      }

      isFieldTouched = (name) => {
        const field = this.fields.find(component => component.props.name === name);

        if (!field) return false;
        return !field.isPristine();
      }

      isFieldValidating = (name) => {
        const field = this.fields.find(component => component.props.name === name);

        if (!field) return false;
        return !field.isValidating();
      }

      getFieldError = (name) => {
        const field = this.fields.find(component => component.props.name === name);

        if (!field) return '';
        return field.getErrorMessage();
      }

      getFormValues = () => {
        return this.fields.reduce((values, field) => {
          const name = field.props.name;
          values[name] = field.getValue();
          return values;
        }, {});
      }

      getValidationErrors = () => {
        return this.fields.reduce((errors, field) => {
          const name = field.props.name;
          errors[name] = field.getErrorMessage();
          return errors;
        }, {});
      }

      getPristineValues = () => {
        return this.fields.reduce((values, field) => {
          const name = field.props.name;
          values[name] = field.getPristineValue();
          return values;
        }, {});
      }

      isChanged = () => {
        return !isEqual(this.getPristineValues(), this.getFormValues());
      }

      isValidating = () => {
        return some(this.fields, (field) => {
          return field.isValidating();
        });
      }

      isValidValue = (field, value) => {
        return this.runValidation(field, value).isValid;
      }

      runValidation = (field, value = field.getValue()) => {
        const formValidationErrors = this.props.validationErrors;
        const { name, validationError, validationErrors } = field.props;
        const currentValues = this.getFormValues();

        const validationResults = this.runRules(value, currentValues, field._validations);
        const isValid = !validationResults.failed.length &&
          !(formValidationErrors && formValidationErrors[field.props.name]);

        return {
          isValid,
          error: (function () {
            if (isValid) {
              return emptyArray;
            }

            if (validationResults.errors.length) {
              return validationResults.errors;
            }

            if (formValidationErrors && formValidationErrors[name]) {
              return typeof formValidationErrors[name] === 'string' ?
                [formValidationErrors[name]] :
                formValidationErrors[name];
            }

            if (validationResults.failed.length) {
              return validationResults.failed.map((failed) => {
                return validationErrors[failed] ?
                  validationErrors[failed] :
                  validationError;
              }).filter((x, pos, arr) => {
                // Remove duplicates
                return arr.indexOf(x) === pos;
              });
            }
          }())
        };
      }

      runRules = (value, currentValues, validations = []) => {
        const results = {
          errors: [],
          failed: []
        };

        function updateResults(validation, validationMethod) {
          // validation方法可以直接返回错误信息，否则需要返回布尔值表明校验是否成功
          if (typeof validation === 'string') {
            results.errors.push(validation);
            results.failed.push(validationMethod);
          } else if (!validation) {
            results.failed.push(validationMethod);
          }
        }

        Object.keys(validations).forEach((validationMethod) => {
          // validations中不指定function则必须是内置的rule
          if (!validationRules[validationMethod] && typeof validations[validationMethod] !== 'function') {
            throw new Error(`does not have the validation rule: ${validationMethod}`);
          }

          // 使用自定义校验方法或内置校验方法（可以按需添加）
          if (typeof validations[validationMethod] === 'function') {
            const validation = validations[validationMethod](currentValues, value);
            updateResults(validation, validationMethod);
          } else if (typeof validations[validationMethod] !== 'function') {
            const validation = validationRules[validationMethod](currentValues, value, validations[validationMethod]);
            updateResults(validation, validationMethod);
          }
        });

        return results;
      }

      validate = (field) => {
        // 初始化时调用validate不触发onChange
        if (this._isMounted) {
          this.props.onChange(this.getFormValues(), this.isChanged());
        }

        const validation = this.runValidation(field);

        field.setState({
          _isValid: validation.isValid,
          _validationError: validation.error,
          _externalError: null
        }, this.validateForm);
      }

      asyncValidate = (field, value) => {
        const { asyncValidation } = field.props;
        const values = this.getFormValues();

        if (!field.isValid()) return;

        field.setState({
          _isValidating: true
        });

        const promise = asyncValidation(values, value);
        if (!isPromise(promise)) {
          throw new Error('asyncValidation function must return a promise');
        }

        const handleResult = rejected => error => {
          field.setState({
            _isValidating: false,
            _isValid: !rejected,
            _asyncValidationError: error ? [error] : null
          });
        };

        return promise.then(handleResult(false), handleResult(true));
      }

      validateForm = () => {
        const onValidationComplete = () => {
          const allIsValid = this.fields.every(field => {
            return field.isValid();
          });

          this.setState({
            isFormValid: allIsValid
          });

          if (allIsValid) {
            this.props.onValid();
          } else {
            this.props.onInvalid();
          }
        };

        this.fields.forEach((field, index) => {
          const { _externalError, _asyncValidationError } = field.state;
          const validation = this.runValidation(field);
          if (validation.isValid && (_externalError || _asyncValidationError)) {
            validation.isValid = false;
          }

          field.setState({
            _isValid: validation.isValid,
            _validationError: validation.error,
            _externalError: !validation.isValid && _externalError ?
              _externalError :
              null
          }, index === this.fields.length - 1 ? onValidationComplete : null);
        });
      }

      attachToForm = (field) => {
        if (this.fields.indexOf(field) < 0) {
          this.fields.push(field);
        }
        // form初始化时不校验，后续动态添加的元素再校验
        this._isMounted && this.validate(field);
      }

      detachFromForm = (field) => {
        const fieldPos = this.fields.indexOf(field);
        if (fieldPos >= 0) {
          this.fields.splice(fieldPos, 1);
        }
        this.validateForm();
      }

      render() {
        const passableProps = omit(this.props, [
          'validationErrors',
          'handleSubmit',
          'onChange'
        ]);

        return createElement(WrappedComponent, {
          ...passableProps,
          handleSubmit: this.submit,
          zentForm: {
            getFormValues: this.getFormValues,
            getFieldError: this.getFieldError,
            setFieldExternalErrors: this.setFieldExternalErrors,
            resetFieldsValue: this.resetFieldsValue,
            setFormPristine: this.setFormPristine,
            isFieldTouched: this.isFieldTouched,
            isFieldValidating: this.isFieldValidating,
            isValid: this.isValid,
            isSubmitting: this.isSubmitting
          }
        });
      }
    };
  };
};

export default createForm;
<|endoftext|>