mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 15:35:36 +03:00
Fix nx lint setup (#3234)
* Fix nx lint setup * Fixes * Fixes * Add missing metadata Fixes Fix Fixes * Fix
This commit is contained in:
parent
52d4f8e466
commit
c15e138d72
81
package.json
81
package.json
@ -19,86 +19,15 @@
|
||||
"tools/eslint-rules"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"patch-package": "^8.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^3.3.1",
|
||||
"@graphql-codegen/client-preset": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@nx/eslint": "17.2.7",
|
||||
"@nx/eslint-plugin": "17.2.7",
|
||||
"@nx/jest": "17.2.7",
|
||||
"@nx/js": "17.2.7",
|
||||
"@storybook/addon-actions": "^7.6.3",
|
||||
"@storybook/addon-coverage": "^1.0.0",
|
||||
"@storybook/addon-essentials": "^7.6.3",
|
||||
"@storybook/addon-interactions": "^7.6.3",
|
||||
"@storybook/addon-links": "^7.6.3",
|
||||
"@storybook/addon-onboarding": "^1.0.9",
|
||||
"@storybook/addon-themes": "^7.6.3",
|
||||
"@storybook/blocks": "^7.6.3",
|
||||
"@storybook/react": "^7.6.3",
|
||||
"@storybook/react-vite": "^7.6.3",
|
||||
"@storybook/test": "^7.6.3",
|
||||
"@storybook/test-runner": "^0.16.0",
|
||||
"@stylistic/eslint-plugin": "^1.5.0",
|
||||
"@swc-node/register": "~1.6.7",
|
||||
"@swc/core": "~1.3.100",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.snakecase": "^4.1.9",
|
||||
"@types/luxon": "^3.3.0",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-datepicker": "^4.11.2",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/scroll-into-view": "^1.16.0",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"@typescript-eslint/utils": "^6.9.1",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"chromatic": "^6.18.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"cross-var": "^1.1.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-storybook": "^0.6.15",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"msw": "^2.0.11",
|
||||
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
|
||||
"nx": "17.2.7",
|
||||
"prettier": "3.1.0",
|
||||
"storybook": "^7.6.3",
|
||||
"storybook-addon-cookie": "^3.1.0",
|
||||
"storybook-addon-pseudo-states": "^2.1.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"patch-package": "^8.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
"nx": "^17.2.8"
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,12 @@ prod-postgres-build:
|
||||
prod-postgres-run:
|
||||
@docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres --name twenty-postgres twenty-postgres
|
||||
|
||||
prod-website-build:
|
||||
@cd ../.. && docker build -f ./packages/twenty-docker/prod/twenty-website/Dockerfile --build-arg GITHUB_TOKEN=REPLACE_ME --tag twenty-website . && cd -
|
||||
|
||||
prod-website-run:
|
||||
@docker run -d -p 3000:3000 --name twenty-website twenty-website
|
||||
|
||||
release-front:
|
||||
@cd ../.. && docker buildx build \
|
||||
--push \
|
||||
|
@ -10,7 +10,6 @@ COPY ./tools/eslint-rules /app/tools/eslint-rules
|
||||
COPY ./packages/twenty-server /app/packages/twenty-server
|
||||
RUN yarn
|
||||
|
||||
RUN yarn
|
||||
RUN yarn nx build twenty-server
|
||||
|
||||
WORKDIR /app/packages/twenty-server
|
||||
|
30
packages/twenty-docker/prod/twenty-website/Dockerfile
Normal file
30
packages/twenty-docker/prod/twenty-website/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
FROM node:18.17.1-alpine as twenty-website-build
|
||||
|
||||
ARG GITHUB_TOKEN
|
||||
ARG BASE_URL
|
||||
|
||||
ENV GITHUB_TOKEN=$GITHUB_TOKEN
|
||||
ENV BASE_URL=$BASE_URL
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package.json .
|
||||
COPY ./.eslintrc.js .
|
||||
COPY ./yarn.lock .
|
||||
COPY ./.yarnrc.yml .
|
||||
COPY ./.yarn/releases /app/.yarn/releases
|
||||
COPY ./packages/twenty-website /app/packages/twenty-website
|
||||
|
||||
RUN yarn
|
||||
RUN yarn nx build twenty-website
|
||||
|
||||
FROM node:18.17.1-alpine as twenty-website
|
||||
|
||||
WORKDIR /app/packages/twenty-website
|
||||
|
||||
COPY --from=twenty-website-build /app /app
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
|
||||
LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the website."
|
||||
|
||||
CMD ["/bin/sh", "-c", "yarn nx start twenty-website"]
|
@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"nx": "NX_DEFAULT_PROJECT=twenty-docs node ../../node_modules/nx/bin/nx.js",
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start --host 0.0.0.0 --port 5001",
|
||||
"build": "docusaurus build",
|
||||
|
@ -80,7 +80,7 @@ module.exports = {
|
||||
rules: {
|
||||
'storybook/no-uninstalled-addons': [
|
||||
'error',
|
||||
{ packageJsonLocation: path.resolve('../../package.json') },
|
||||
{ packageJsonLocation: path.resolve('package.json') },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"nx": "NX_DEFAULT_PROJECT=twenty-front node ../../node_modules/nx/bin/nx.js",
|
||||
"start": "vite --host",
|
||||
"start:clean": "yarn start --force",
|
||||
"build": "tsc && vite build && yarn build:inject-runtime-env",
|
||||
@ -94,6 +95,79 @@
|
||||
"xlsx-ugnis": "^0.19.3",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^3.3.1",
|
||||
"@graphql-codegen/client-preset": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@storybook/addon-actions": "^7.6.3",
|
||||
"@storybook/addon-coverage": "^1.0.0",
|
||||
"@storybook/addon-essentials": "^7.6.7",
|
||||
"@storybook/addon-interactions": "^7.6.7",
|
||||
"@storybook/addon-links": "^7.6.7",
|
||||
"@storybook/addon-onboarding": "^1.0.10",
|
||||
"@storybook/addon-themes": "^7.6.7",
|
||||
"@storybook/blocks": "^7.6.3",
|
||||
"@storybook/react": "^7.6.3",
|
||||
"@storybook/react-vite": "^7.6.3",
|
||||
"@storybook/test": "^7.6.3",
|
||||
"@storybook/test-runner": "^0.16.0",
|
||||
"@stylistic/eslint-plugin": "^1.5.0",
|
||||
"@swc-node/register": "~1.6.7",
|
||||
"@swc/core": "~1.3.100",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/lodash.camelcase": "^4.3.7",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.snakecase": "^4.1.9",
|
||||
"@types/luxon": "^3.3.0",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-datepicker": "^4.11.2",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/scroll-into-view": "^1.16.0",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"@typescript-eslint/utils": "^6.9.1",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"chromatic": "^6.18.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"cross-var": "^1.1.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.4",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-storybook": "^0.6.15",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"msw": "^2.0.11",
|
||||
"msw-storybook-addon": "2.0.0--canary.122.b3ed3b1.0",
|
||||
"prettier": "3.1.0",
|
||||
"storybook": "^7.6.3",
|
||||
"storybook-addon-cookie": "^3.2.0",
|
||||
"storybook-addon-pseudo-states": "^2.1.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.16.0",
|
||||
"npm": "please-use-yarn",
|
||||
|
@ -182,6 +182,12 @@ export enum FieldMetadataType {
|
||||
Uuid = 'UUID'
|
||||
}
|
||||
|
||||
export type FullName = {
|
||||
__typename?: 'FullName';
|
||||
firstName: Scalars['String']['output'];
|
||||
lastName: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type IdFilterComparison = {
|
||||
eq?: InputMaybe<Scalars['ID']['input']>;
|
||||
gt?: InputMaybe<Scalars['ID']['input']>;
|
||||
@ -389,6 +395,11 @@ export enum RelationMetadataType {
|
||||
OneToOne = 'ONE_TO_ONE'
|
||||
}
|
||||
|
||||
export type Sentry = {
|
||||
__typename?: 'Sentry';
|
||||
dsn: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Support = {
|
||||
__typename?: 'Support';
|
||||
supportDriver: Scalars['String']['output'];
|
||||
@ -454,7 +465,7 @@ export type User = {
|
||||
lastName: Scalars['String']['output'];
|
||||
passwordHash?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
workspaceMember: UserWorkspaceMember;
|
||||
workspaceMember: WorkspaceMember;
|
||||
};
|
||||
|
||||
export type UserEdge = {
|
||||
@ -465,21 +476,6 @@ export type UserEdge = {
|
||||
node: User;
|
||||
};
|
||||
|
||||
export type UserWorkspaceMember = {
|
||||
__typename?: 'UserWorkspaceMember';
|
||||
avatarUrl?: Maybe<Scalars['String']['output']>;
|
||||
colorScheme: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
locale: Scalars['String']['output'];
|
||||
name: UserWorkspaceMemberName;
|
||||
};
|
||||
|
||||
export type UserWorkspaceMemberName = {
|
||||
__typename?: 'UserWorkspaceMemberName';
|
||||
firstName: Scalars['String']['output'];
|
||||
lastName: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
allowImpersonation: Scalars['Boolean']['output'];
|
||||
@ -502,6 +498,15 @@ export type WorkspaceEdge = {
|
||||
node: Workspace;
|
||||
};
|
||||
|
||||
export type WorkspaceMember = {
|
||||
__typename?: 'WorkspaceMember';
|
||||
avatarUrl?: Maybe<Scalars['String']['output']>;
|
||||
colorScheme: Scalars['String']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
locale: Scalars['String']['output'];
|
||||
name: FullName;
|
||||
};
|
||||
|
||||
export type Field = {
|
||||
__typename?: 'field';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
|
@ -6,6 +6,7 @@
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"nx": "NX_DEFAULT_PROJECT=twenty-server node ../../node_modules/nx/bin/nx.js",
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "yarn prebuild && nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
@ -17,9 +18,9 @@
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register ../../node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "./scripts/run-integration.sh",
|
||||
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
|
||||
"typeorm": "ts-node -r tsconfig-paths/register ../../node_modules/typeorm/cli.js",
|
||||
"typeorm:migrate": "yarn typeorm migration:run -d ./src/database/typeorm/metadata/metadata.datasource.ts && yarn typeorm migration:run -d ./src/database/typeorm/core/core.datasource.ts",
|
||||
"database:init": "yarn database:setup && yarn database:seed:dev",
|
||||
"database:setup": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate",
|
||||
@ -131,8 +132,7 @@
|
||||
"@types/uuid": "^9.0.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-loader": "^9.2.3",
|
||||
"tsconfig-paths": "4.1.0"
|
||||
"ts-loader": "^9.2.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"graphql": "16.8.0"
|
||||
|
7
packages/twenty-website/.eslintrc.js
Normal file
7
packages/twenty-website/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['../../.eslintrc.js'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'prefer-arrow/prefer-arrow-functions': 'off',
|
||||
},
|
||||
};
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
]
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "auto"
|
||||
}
|
@ -6,7 +6,7 @@ This is not related in anyway to the main app, which you can find in twenty-fron
|
||||
|
||||
## Getting Started
|
||||
|
||||
We're using Nest.JS
|
||||
We're using Next.JS
|
||||
|
||||
From the root directory:
|
||||
```bash
|
||||
|
@ -3,6 +3,7 @@
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
@ -14,14 +15,18 @@
|
||||
"@jsdevtools/rehype-toc": "^3.0.2",
|
||||
"@nivo/calendar": "^0.84.0",
|
||||
"@nivo/core": "^0.84.0",
|
||||
"@nx/eslint-plugin": "^17.2.8",
|
||||
"@octokit/graphql": "^7.0.2",
|
||||
"@stoplight/elements": "^7.16.2",
|
||||
"@tabler/icons-react": "^2.44.0",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"graphiql": "^3.0.10",
|
||||
"graphql": "^16.8.1",
|
||||
"next": "14.0.4",
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^4.12.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-behead": "^3.1.0",
|
||||
"remark-gfm": "^3.0.1"
|
||||
|
@ -1,4 +1,4 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import Link from 'next/link';
|
||||
@ -6,8 +6,7 @@ import Link from 'next/link';
|
||||
export interface User {
|
||||
login: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const AvatarGridContainer = styled.div`
|
||||
display: grid;
|
||||
@ -35,7 +34,9 @@ const AvatarItem = styled.div`
|
||||
text-align: center;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease, visibility 0.3s;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
visibility 0.3s;
|
||||
}
|
||||
|
||||
&:hover .username {
|
||||
@ -46,12 +47,14 @@ const AvatarItem = styled.div`
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
||||
const AvatarGrid = ({ users }: { users: User[] }) => {
|
||||
return (
|
||||
<AvatarGridContainer>
|
||||
{users.map(user => (
|
||||
<Link href={`/developers/contributors/${user.login}`} key={`l_${user.login}`}>
|
||||
{users.map((user) => (
|
||||
<Link
|
||||
href={`/developers/contributors/${user.login}`}
|
||||
key={`l_${user.login}`}
|
||||
>
|
||||
<AvatarItem key={user.login}>
|
||||
<img src={user.avatarUrl} alt={user.login} />
|
||||
<span className="username">{user.login}</span>
|
||||
|
@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { Logo } from './Logo';
|
||||
import { DiscordIcon, GithubIcon2, LinkedInIcon, XIcon } from './Icons';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { DiscordIcon, GithubIcon2, LinkedInIcon, XIcon } from './Icons';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
const FooterContainer = styled.div`
|
||||
padding: 64px 96px 64px 96px;
|
||||
display: flex;
|
||||
|
@ -1,12 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { Logo } from './Logo';
|
||||
import { IBM_Plex_Mono } from 'next/font/google';
|
||||
import { DiscordIcon, GithubIcon, GithubIcon2, XIcon } from './Icons';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { ExternalArrow } from '@/app/components/ExternalArrow';
|
||||
|
||||
import { DiscordIcon, GithubIcon, GithubIcon2, XIcon } from './Icons';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
const IBMPlexMono = IBM_Plex_Mono({
|
||||
weight: '500',
|
||||
subsets: ['latin'],
|
||||
|
@ -1,12 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { Logo } from './Logo';
|
||||
import { IBM_Plex_Mono } from 'next/font/google';
|
||||
import { GithubIcon } from './Icons';
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IBM_Plex_Mono } from 'next/font/google';
|
||||
|
||||
import { ExternalArrow } from '@/app/components/ExternalArrow';
|
||||
|
||||
import { GithubIcon } from './Icons';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
const IBMPlexMono = IBM_Plex_Mono({
|
||||
weight: '500',
|
||||
subsets: ['latin'],
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
|
||||
import TokenForm, {
|
||||
TokenFormProps,
|
||||
} from '@/app/components/PlaygroundTokenForm';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
|
||||
type PlaygroundProps = TokenFormProps & {
|
||||
children?: ReactNode;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { parseJson } from 'nx/src/utils/json';
|
||||
import { TbLoader2 } from 'react-icons/tb';
|
||||
import styled from '@emotion/styled';
|
||||
import { parseJson } from 'nx/src/utils/json';
|
||||
|
||||
export type TokenFormProps = {
|
||||
setOpenApiJson?: (json: object) => void;
|
||||
|
@ -1,9 +1,11 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { ResponsiveTimeRange } from '@nivo/calendar'
|
||||
import { ResponsiveTimeRange } from '@nivo/calendar';
|
||||
|
||||
export const ActivityLog = ({ data }: { data: { value: number, day: string }[] }) => {
|
||||
return <ResponsiveTimeRange
|
||||
data={data}
|
||||
/>;
|
||||
}
|
||||
export const ActivityLog = ({
|
||||
data,
|
||||
}: {
|
||||
data: { value: number; day: string }[];
|
||||
}) => {
|
||||
return <ResponsiveTimeRange data={data} />;
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
import Image from 'next/image';
|
||||
import Database from 'better-sqlite3';
|
||||
import AvatarGrid from '@/app/components/AvatarGrid';
|
||||
import { ResponsiveTimeRange } from '@nivo/calendar'
|
||||
import { ActivityLog } from './components/ActivityLog';
|
||||
import { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { ActivityLog } from './components/ActivityLog';
|
||||
|
||||
interface Contributor {
|
||||
login: string;
|
||||
@ -11,21 +10,22 @@ interface Contributor {
|
||||
pullRequestCount: number;
|
||||
}
|
||||
|
||||
|
||||
export function generateMetadata({ params }: { params: { slug: string } }): Metadata {
|
||||
export function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { slug: string };
|
||||
}): Metadata {
|
||||
return {
|
||||
title: params.slug + ' | Contributors',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default async function (
|
||||
{ params }: { params: { slug: string } }) {
|
||||
|
||||
export default async function ({ params }: { params: { slug: string } }) {
|
||||
const db = new Database('db.sqlite', { readonly: true });
|
||||
|
||||
const contributor = db.prepare(`
|
||||
const contributor = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
u.login,
|
||||
u.avatarUrl,
|
||||
@ -35,9 +35,13 @@ export default async function (
|
||||
users u
|
||||
WHERE
|
||||
u.login = :user_id
|
||||
`).get({'user_id' : params.slug}) as Contributor;
|
||||
`,
|
||||
)
|
||||
.get({ user_id: params.slug }) as Contributor;
|
||||
|
||||
const pullRequestActivity = db.prepare(`
|
||||
const pullRequestActivity = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) as value,
|
||||
DATE(createdAt) as day
|
||||
@ -49,11 +53,13 @@ const pullRequestActivity = db.prepare(`
|
||||
DATE(createdAt)
|
||||
ORDER BY
|
||||
DATE(createdAt)
|
||||
`).all({'user_id': params.slug}) as { value: number, day: string }[];
|
||||
`,
|
||||
)
|
||||
.all({ user_id: params.slug }) as { value: number; day: string }[];
|
||||
|
||||
|
||||
|
||||
const pullRequestList = db.prepare(`
|
||||
const pullRequestList = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
@ -69,26 +75,40 @@ const pullRequestList = db.prepare(`
|
||||
authorId = (SELECT id FROM users WHERE login = :user_id)
|
||||
ORDER BY
|
||||
DATE(createdAt) DESC
|
||||
`).all({'user_id': params.slug}) as { title: string, createdAt: string, url: string }[];
|
||||
|
||||
`,
|
||||
)
|
||||
.all({ user_id: params.slug }) as {
|
||||
title: string;
|
||||
createdAt: string;
|
||||
url: string;
|
||||
}[];
|
||||
|
||||
db.close();
|
||||
|
||||
|
||||
return (
|
||||
<div style={{maxWidth: '900px', display: 'flex', padding: '40px', gap: '24px'}}>
|
||||
<div style={{ flexDirection: 'column', width: '240px'}}>
|
||||
<Image src={contributor.avatarUrl} alt={contributor.login} width={240} height={240} />
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '900px',
|
||||
display: 'flex',
|
||||
padding: '40px',
|
||||
gap: '24px',
|
||||
}}
|
||||
>
|
||||
<div style={{ flexDirection: 'column', width: '240px' }}>
|
||||
<Image
|
||||
src={contributor.avatarUrl}
|
||||
alt={contributor.login}
|
||||
width={240}
|
||||
height={240}
|
||||
/>
|
||||
<h1>{contributor.login}</h1>
|
||||
</div>
|
||||
<div style={{flexDirection: 'column'}}>
|
||||
<div style={{width: '450px', height: '200px'}}>
|
||||
<ActivityLog
|
||||
data={pullRequestActivity}
|
||||
/>
|
||||
<div style={{ flexDirection: 'column' }}>
|
||||
<div style={{ width: '450px', height: '200px' }}>
|
||||
<ActivityLog data={pullRequestActivity} />
|
||||
</div>
|
||||
<div style={{width: '450px'}}>
|
||||
{pullRequestList.map(pr => (
|
||||
<div style={{ width: '450px' }}>
|
||||
{pullRequestList.map((pr) => (
|
||||
<div>
|
||||
<a href={pr.url}>{pr.title}</a>
|
||||
</div>
|
||||
@ -97,4 +117,4 @@ const pullRequestList = db.prepare(`
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { slug: string } }) {
|
||||
{ params }: { params: { slug: string } },
|
||||
) {
|
||||
const db = new Database('db.sqlite', { readonly: true });
|
||||
|
||||
if(params.slug !== 'users' && params.slug !== 'labels' && params.slug !== 'pullRequests' && params.slug !== 'issues') {
|
||||
if (
|
||||
params.slug !== 'users' &&
|
||||
params.slug !== 'labels' &&
|
||||
params.slug !== 'pullRequests' &&
|
||||
params.slug !== 'issues'
|
||||
) {
|
||||
return Response.json({ error: 'Invalid table name' }, { status: 400 });
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
const db = new Database('db.sqlite', { verbose: console.log });
|
||||
|
||||
@ -83,8 +83,13 @@ const query = graphql.defaults({
|
||||
},
|
||||
});
|
||||
|
||||
async function fetchData(cursor: string | null = null, isIssues: boolean = false, accumulatedData: Array<PullRequestNode | IssueNode> = []): Promise<Array<PullRequestNode | IssueNode>> {
|
||||
const { repository } = await query<RepoData>(`
|
||||
async function fetchData(
|
||||
cursor: string | null = null,
|
||||
isIssues: boolean = false,
|
||||
accumulatedData: Array<PullRequestNode | IssueNode> = [],
|
||||
): Promise<Array<PullRequestNode | IssueNode>> {
|
||||
const { repository } = await query<RepoData>(
|
||||
`
|
||||
query ($cursor: String) {
|
||||
repository(owner: "twentyhq", name: "twenty") {
|
||||
pullRequests(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) @skip(if: ${isIssues}) {
|
||||
@ -148,10 +153,17 @@ async function fetchData(cursor: string | null = null, isIssues: boolean = false
|
||||
}
|
||||
}
|
||||
}
|
||||
`, { cursor });
|
||||
`,
|
||||
{ cursor },
|
||||
);
|
||||
|
||||
const newAccumulatedData: Array<PullRequestNode | IssueNode> = [...accumulatedData, ...(isIssues ? repository.issues.nodes : repository.pullRequests.nodes)];
|
||||
const pageInfo = isIssues ? repository.issues.pageInfo : repository.pullRequests.pageInfo;
|
||||
const newAccumulatedData: Array<PullRequestNode | IssueNode> = [
|
||||
...accumulatedData,
|
||||
...(isIssues ? repository.issues.nodes : repository.pullRequests.nodes),
|
||||
];
|
||||
const pageInfo = isIssues
|
||||
? repository.issues.pageInfo
|
||||
: repository.pullRequests.pageInfo;
|
||||
|
||||
if (pageInfo.hasNextPage) {
|
||||
return fetchData(pageInfo.endCursor, isIssues, newAccumulatedData);
|
||||
@ -173,11 +185,12 @@ async function fetchAssignableUsers(): Promise<Set<string>> {
|
||||
}
|
||||
`);
|
||||
|
||||
return new Set(repository.assignableUsers.nodes.map(user => user.login));
|
||||
return new Set(repository.assignableUsers.nodes.map((user) => user.login));
|
||||
}
|
||||
|
||||
const initDb = () => {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS pullRequests (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
@ -190,9 +203,11 @@ const initDb = () => {
|
||||
authorId TEXT,
|
||||
FOREIGN KEY (authorId) REFERENCES users(id)
|
||||
);
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS issues (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
@ -204,9 +219,11 @@ const initDb = () => {
|
||||
authorId TEXT,
|
||||
FOREIGN KEY (authorId) REFERENCES users(id)
|
||||
);
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
login TEXT,
|
||||
@ -214,39 +231,44 @@ const initDb = () => {
|
||||
url TEXT,
|
||||
isEmployee BOOLEAN
|
||||
);
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS labels (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
color TEXT,
|
||||
description TEXT
|
||||
);
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS pullRequestLabels (
|
||||
pullRequestId TEXT,
|
||||
labelId TEXT,
|
||||
FOREIGN KEY (pullRequestId) REFERENCES pullRequests(id),
|
||||
FOREIGN KEY (labelId) REFERENCES labels(id)
|
||||
);
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS issueLabels (
|
||||
issueId TEXT,
|
||||
labelId TEXT,
|
||||
FOREIGN KEY (issueId) REFERENCES issues(id),
|
||||
FOREIGN KEY (labelId) REFERENCES labels(id)
|
||||
);
|
||||
`).run();
|
||||
|
||||
`,
|
||||
).run();
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
|
||||
initDb();
|
||||
|
||||
// TODO if we ever hit API Rate Limiting
|
||||
@ -254,21 +276,51 @@ export async function GET() {
|
||||
const lastIssueCursor = null;
|
||||
|
||||
const assignableUsers = await fetchAssignableUsers();
|
||||
const prs = await fetchData(lastPRCursor) as Array<PullRequestNode>;
|
||||
const issues = await fetchData(lastIssueCursor, true) as Array<IssueNode>;
|
||||
const prs = (await fetchData(lastPRCursor)) as Array<PullRequestNode>;
|
||||
const issues = (await fetchData(lastIssueCursor, true)) as Array<IssueNode>;
|
||||
|
||||
const insertPR = db.prepare('INSERT INTO pullRequests (id, title, body, url, createdAt, updatedAt, closedAt, mergedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
||||
const insertIssue = db.prepare('INSERT INTO issues (id, title, body, url, createdAt, updatedAt, closedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
||||
const insertUser = db.prepare('INSERT INTO users (id, login, avatarUrl, url, isEmployee) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
||||
const insertLabel = db.prepare('INSERT INTO labels (id, name, color, description) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
||||
const insertPullRequestLabel = db.prepare('INSERT INTO pullRequestLabels (pullRequestId, labelId) VALUES (?, ?)');
|
||||
const insertIssueLabel = db.prepare('INSERT INTO issueLabels (issueId, labelId) VALUES (?, ?)');
|
||||
const insertPR = db.prepare(
|
||||
'INSERT INTO pullRequests (id, title, body, url, createdAt, updatedAt, closedAt, mergedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING',
|
||||
);
|
||||
const insertIssue = db.prepare(
|
||||
'INSERT INTO issues (id, title, body, url, createdAt, updatedAt, closedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING',
|
||||
);
|
||||
const insertUser = db.prepare(
|
||||
'INSERT INTO users (id, login, avatarUrl, url, isEmployee) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING',
|
||||
);
|
||||
const insertLabel = db.prepare(
|
||||
'INSERT INTO labels (id, name, color, description) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO NOTHING',
|
||||
);
|
||||
const insertPullRequestLabel = db.prepare(
|
||||
'INSERT INTO pullRequestLabels (pullRequestId, labelId) VALUES (?, ?)',
|
||||
);
|
||||
const insertIssueLabel = db.prepare(
|
||||
'INSERT INTO issueLabels (issueId, labelId) VALUES (?, ?)',
|
||||
);
|
||||
|
||||
for (const pr of prs) {
|
||||
console.log(pr);
|
||||
if(pr.author == null) { continue; }
|
||||
insertUser.run(pr.author.resourcePath, pr.author.login, pr.author.avatarUrl, pr.author.url, assignableUsers.has(pr.author.login) ? 1 : 0);
|
||||
insertPR.run(pr.id, pr.title, pr.body, pr.url, pr.createdAt, pr.updatedAt, pr.closedAt, pr.mergedAt, pr.author.resourcePath);
|
||||
if (pr.author == null) {
|
||||
continue;
|
||||
}
|
||||
insertUser.run(
|
||||
pr.author.resourcePath,
|
||||
pr.author.login,
|
||||
pr.author.avatarUrl,
|
||||
pr.author.url,
|
||||
assignableUsers.has(pr.author.login) ? 1 : 0,
|
||||
);
|
||||
insertPR.run(
|
||||
pr.id,
|
||||
pr.title,
|
||||
pr.body,
|
||||
pr.url,
|
||||
pr.createdAt,
|
||||
pr.updatedAt,
|
||||
pr.closedAt,
|
||||
pr.mergedAt,
|
||||
pr.author.resourcePath,
|
||||
);
|
||||
|
||||
for (const label of pr.labels.nodes) {
|
||||
insertLabel.run(label.id, label.name, label.color, label.description);
|
||||
@ -277,10 +329,27 @@ export async function GET() {
|
||||
}
|
||||
|
||||
for (const issue of issues) {
|
||||
if(issue.author == null) { continue; }
|
||||
insertUser.run(issue.author.resourcePath, issue.author.login, issue.author.avatarUrl, issue.author.url, assignableUsers.has(issue.author.login) ? 1 : 0);
|
||||
if (issue.author == null) {
|
||||
continue;
|
||||
}
|
||||
insertUser.run(
|
||||
issue.author.resourcePath,
|
||||
issue.author.login,
|
||||
issue.author.avatarUrl,
|
||||
issue.author.url,
|
||||
assignableUsers.has(issue.author.login) ? 1 : 0,
|
||||
);
|
||||
|
||||
insertIssue.run(issue.id, issue.title, issue.body, issue.url, issue.createdAt, issue.updatedAt, issue.closedAt, issue.author.resourcePath);
|
||||
insertIssue.run(
|
||||
issue.id,
|
||||
issue.title,
|
||||
issue.body,
|
||||
issue.url,
|
||||
issue.createdAt,
|
||||
issue.updatedAt,
|
||||
issue.closedAt,
|
||||
issue.author.resourcePath,
|
||||
);
|
||||
|
||||
for (const label of issue.labels.nodes) {
|
||||
insertLabel.run(label.id, label.name, label.color, label.description);
|
||||
@ -290,5 +359,5 @@ export async function GET() {
|
||||
|
||||
db.close();
|
||||
|
||||
return new Response("Data synced", { status: 200 });
|
||||
};
|
||||
return new Response('Data synced', { status: 200 });
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Image from 'next/image';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
import AvatarGrid from '@/app/components/AvatarGrid';
|
||||
|
||||
interface Contributor {
|
||||
@ -9,11 +9,11 @@ interface Contributor {
|
||||
}
|
||||
|
||||
const Contributors = async () => {
|
||||
|
||||
|
||||
const db = new Database('db.sqlite', { readonly: true });
|
||||
|
||||
const contributors = db.prepare(`SELECT
|
||||
const contributors = db
|
||||
.prepare(
|
||||
`SELECT
|
||||
u.login,
|
||||
u.avatarUrl,
|
||||
COUNT(pr.id) AS pullRequestCount
|
||||
@ -25,8 +25,9 @@ const Contributors = async () => {
|
||||
u.id
|
||||
ORDER BY
|
||||
pullRequestCount DESC;
|
||||
`).all() as Contributor[];
|
||||
|
||||
`,
|
||||
)
|
||||
.all() as Contributor[];
|
||||
|
||||
db.close();
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import 'graphiql/graphiql.css';
|
||||
|
||||
// Create a named function for your component
|
||||
|
@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import createCache from '@emotion/cache';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import createCache from '@emotion/cache';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
|
||||
export default function RootStyleRegistry({ children }) {
|
||||
const [{ cache, flush }] = useState(() => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import { ReactElement } from 'react';
|
||||
import gfm from 'remark-gfm';
|
||||
import rehypeToc from '@jsdevtools/rehype-toc';
|
||||
import fs from 'fs';
|
||||
import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import path from 'path';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
import gfm from 'remark-gfm';
|
||||
|
||||
interface ItemInfo {
|
||||
title: string;
|
||||
|
@ -1,9 +1,12 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Metadata } from 'next';
|
||||
import { Gabarito } from 'next/font/google';
|
||||
import EmotionRootStyleRegistry from './emotion-root-style-registry';
|
||||
import { HeaderDesktop } from './components/HeaderDesktop';
|
||||
import { FooterDesktop } from './components/FooterDesktop';
|
||||
|
||||
import { HeaderMobile } from '@/app/components/HeaderMobile';
|
||||
|
||||
import { FooterDesktop } from './components/FooterDesktop';
|
||||
import { HeaderDesktop } from './components/HeaderDesktop';
|
||||
import EmotionRootStyleRegistry from './emotion-root-style-registry';
|
||||
|
||||
import './layout.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Metadata } from 'next';
|
||||
import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import gfm from 'remark-gfm';
|
||||
import { ContentContainer } from '../components/ContentContainer';
|
||||
import remarkBehead from 'remark-behead';
|
||||
import type { Metadata } from 'next';
|
||||
import gfm from 'remark-gfm';
|
||||
|
||||
import { ContentContainer } from '../components/ContentContainer';
|
||||
|
||||
interface Release {
|
||||
id: number;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { ContentContainer } from '@/app/components/ContentContainer';
|
||||
import { getPosts, Directory, FileContent } from '@/app/get-posts';
|
||||
import Link from 'next/link';
|
||||
import * as TablerIcons from '@tabler/icons-react';
|
||||
import { FunctionComponent } from 'react';
|
||||
import * as TablerIcons from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ContentContainer } from '@/app/components/ContentContainer';
|
||||
import { Directory, FileContent, getPosts } from '@/app/get-posts';
|
||||
|
||||
function loadIcon(iconName?: string) {
|
||||
const name = iconName ? iconName : 'IconCategory';
|
||||
|
Loading…
Reference in New Issue
Block a user