Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: repo collection (final) #730

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"description": "Hypertrons Chromium Extension",
"license": "Apache",
"engines": {
"node": ">=16.14"
"node": ">=18"
},
"scripts": {
"build": "cross-env NODE_ENV='production' BABEL_ENV='production' node utils/build.js",
Expand All @@ -28,6 +28,7 @@
"antd": "^5.9.1",
"buffer": "^6.0.3",
"colorthief": "^2.4.0",
"constate": "^3.3.2",
"delay": "^5.0.0",
"dom-loaded": "^3.0.0",
"echarts": "^5.3.0",
Expand Down
5 changes: 5 additions & 0 deletions src/api/repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const metricNameMap = new Map([
['developer_network', 'developer_network'],
['repo_network', 'repo_network'],
['activity_details', 'activity_details'],
['issue_response_time', 'issue_response_time'],
]);

export const getActivity = async (repo: string) => {
Expand Down Expand Up @@ -88,3 +89,7 @@ export const getRepoNetwork = async (repo: string) => {
export const getActivityDetails = async (repo: string) => {
return getMetricByName(repo, metricNameMap, 'activity_details');
};

export const getIssueResponseTime = async (repo: string) => {
return getMetricByName(repo, metricNameMap, 'issue_response_time');
};
14 changes: 14 additions & 0 deletions src/helpers/get-newest-month.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const getNewestMonth = () => {
const now = new Date();
if (now.getDate() === 1) {
// data for last month is not ready in the first day of the month (#595)
now.setDate(0); // a way to let month - 1
}
now.setDate(0); // see issue #632

return (
now.getFullYear() + '-' + (now.getMonth() + 1).toString().padStart(2, '0')
);
};

export default getNewestMonth;
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { useRepoCollectionContext } from '../context';
import { Collection } from '../context/store';

import React, { useEffect, useState } from 'react';

const CheckListItem = (
collection: Collection,
onChange: (collectionId: Collection['id'], checked: boolean) => void,
checked: boolean
) => {
const handleChange = () => {
onChange(collection.id, checked);
};

return (
<div
key={collection.id}
className="form-checkbox mr-3 ml-6 ml-sm-5 mb-2 mt-0"
>
<label className="f5 text-normal">
<input
type="checkbox"
value={collection.id}
checked={checked}
onChange={handleChange}
/>
{collection.name}
</label>
</div>
);
};

/**
* The modal for quickly adding the current repository to existing collections (also for removing)
*/
export const AddToCollections = () => {
const {
currentRepositoryId,
currentRepositoryCollections,
allCollections,
updaters,
hideAddToCollections,
setHideAddToCollections,
setHideCollectionList,
setShowCollectionModal,
} = useRepoCollectionContext();

const [checkedCollectionIds, setCheckedCollectionIds] = useState<
Collection['id'][]
>([]);

const resetCheckboxes = () => {
setCheckedCollectionIds(currentRepositoryCollections.map((c) => c.id));
};

// reset checkboxes when currentRepositoryCollections changes
useEffect(() => {
resetCheckboxes();
}, [currentRepositoryCollections]);

const handleCheckChange = (
collectionId: Collection['id'],
checked: boolean
) => {
if (checked) {
setCheckedCollectionIds(
checkedCollectionIds.filter((id) => id !== collectionId)
);
} else {
setCheckedCollectionIds([...checkedCollectionIds, collectionId]);
}
};

const goToCollectionList = () => {
setHideAddToCollections(true);
setHideCollectionList(false);
};

const apply = () => {
// add/remove relations
const toAdd = checkedCollectionIds.filter(
(id) => !currentRepositoryCollections.some((c) => c.id === id)
);
const toRemove = currentRepositoryCollections.filter(
(c) => !checkedCollectionIds.includes(c.id)
);
toAdd &&
updaters.addRelations(
toAdd.map((id) => ({
collectionId: id,
repositoryId: currentRepositoryId,
}))
);
toRemove &&
updaters.removeRelations(
toRemove.map((c) => ({
collectionId: c.id,
repositoryId: currentRepositoryId,
}))
);

goToCollectionList();
};

const cancel = () => {
resetCheckboxes();

goToCollectionList();
};

const manage = () => {
// open modal to manage collections
setShowCollectionModal(true);
};

// if the ids of currentRepositoryCollections are the same as the ids of selectedCollectionIds, then the "Apply" button should be disabled
let isApplyDisabled: boolean;
if (currentRepositoryCollections.length !== checkedCollectionIds.length) {
isApplyDisabled = false;
} else {
isApplyDisabled = currentRepositoryCollections.every((c) =>
checkedCollectionIds.includes(c.id)
);
}

return (
<div
className="notifications-component-dialog"
hidden={hideAddToCollections}
>
<div className="SelectMenu-modal notifications-component-dialog-modal overflow-visible">
<header className="d-none d-sm-flex flex-items-start pt-1">
<button
className="border-0 px-2 pt-1 m-0 Link--secondary f5"
style={{ backgroundColor: 'transparent' }}
type="button"
onClick={goToCollectionList}
>
<svg
style={{ position: 'relative', left: '2px', top: '1px' }}
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
className="octicon octicon-arrow-left"
>
<path d="M7.78 12.53a.75.75 0 0 1-1.06 0L2.47 8.28a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L4.81 7h7.44a.75.75 0 0 1 0 1.5H4.81l2.97 2.97a.75.75 0 0 1 0 1.06Z"></path>
</svg>
</button>

<h1 className="pt-1 pr-4 pb-0 pl-0 f5 text-bold">
Add to collections
</h1>
</header>
<legend>
<div className="text-small color-fg-muted pt-0 pr-3 pb-3 pl-6 pl-sm-5 border-bottom mb-3">
Select collections you want the current repository to be added to.
</div>
</legend>
{/* Checklist */}
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
{allCollections.map((collection) => {
const checked = checkedCollectionIds.includes(collection.id);
return CheckListItem(collection, handleCheckChange, checked);
})}
</div>
{/* 3 buttons */}
<footer className="SelectMenu-footer">
<div className="pt-2 pb-3 px-3 d-flex flex-justify-start flex-row-reverse">
<button
disabled={isApplyDisabled}
className="btn-primary btn-sm btn ml-2"
onClick={apply}
>
Apply
</button>
<button className="btn-sm btn ml-2" onClick={cancel}>
Cancel
</button>
<button className="btn-sm btn" onClick={manage}>
Manage
</button>
</div>
</footer>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useRepoCollectionContext } from '../context';
import { Collection } from '../context/store';

import React from 'react';

const ListItem = (
collection: Collection,
onClick: (collectionId: Collection['id']) => void
) => {
const handleClick = () => {
onClick(collection.id);
};

return (
<div
key={collection.id}
className="SelectMenu-item flex-items-start btn border-0 rounded-0"
onClick={handleClick}
>
<span className="text-small text-normal wb-break-all">
{collection.name}
</span>
</div>
);
};

/**
* The modal that shows the collections that the repo belongs to
*/
export const CollectionList = () => {
const {
currentRepositoryCollections,
hideCollectionList,
setHideAddToCollections,
setHideCollectionList,
setSelectedCollection,
setShowCollectionModal,
} = useRepoCollectionContext();

const handleCollectionClick = (collectionId: Collection['id']) => {
setSelectedCollection(collectionId);
setShowCollectionModal(true);
};

const goToAddToCollections = () => {
setHideAddToCollections(false);
setHideCollectionList(true);
};

return (
<div className="SelectMenu" hidden={hideCollectionList}>
<div className="SelectMenu-modal">
<button
className="SelectMenu-closeButton position-absolute right-0 m-2"
type="button"
data-toggle-for="collection-button-details"
>
<svg
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
data-view-component="true"
className="octicon octicon-x"
>
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path>
</svg>
</button>
<div className="d-flex flex-column flex-1 overflow-hidden">
<div className="SelectMenu-list">
{/* header */}
<header className="SelectMenu-header">
<h3 className="SelectMenu-title">
Contained in these collections
</h3>
<button
className="SelectMenu-closeButton"
type="button"
aria-label="Close menu"
data-toggle-for="collection-button-details"
>
<svg
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
data-view-component="true"
className="octicon octicon-x"
>
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path>
</svg>
</button>
</header>
{/* list */}
<div className="overflow-y-auto" style={{ maxHeight: '340px' }}>
{currentRepositoryCollections.map((collection) =>
ListItem(collection, handleCollectionClick)
)}
</div>
{/* footer */}
<footer className="SelectMenu-footer p-0 position-sticky">
<div
className="SelectMenu-item btn rounded-0 border-bottom-0 text-normal f6"
onClick={goToAddToCollections}
>
<svg
width="20"
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
data-view-component="true"
className="octicon octicon-plus mr-2 text-center"
>
<path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"></path>
</svg>
Add to collections
</div>
</footer>
</div>
</div>
</div>
</div>
);
};
Loading
Loading