diff --git a/.github/workflows/deploy-Dataspace-dev.yml b/.github/workflows/deploy-Dataspace-dev.yml new file mode 100644 index 00000000..6e77d75d --- /dev/null +++ b/.github/workflows/deploy-Dataspace-dev.yml @@ -0,0 +1,69 @@ +name: Update DataSpace Dev + +on: + push: + branches: ['dev'] +env: + KEYCLOAK_CLIENT_ID: ${{secrets.KEYCLOAK_CLIENT_ID}} + KEYCLOAK_CLIENT_SECRET: ${{secrets.KEYCLOAK_CLIENT_SECRET}} + AUTH_ISSUER: ${{secrets.AUTH_ISSUER}} + NEXTAUTH_URL: 'https://dev.civicdataspace.in/' + NEXT_PUBLIC_NEXTAUTH_URL: 'https://dev.civicdataspace.in/' + NEXTAUTH_SECRET: ${{secrets.NEXTAUTH_SECRET}} + END_SESSION_URL: ${{secrets.END_SESSION_URL}} + REFRESH_TOKEN_URL: ${{secrets.REFRESH_TOKEN_URL}} + NEXT_PUBLIC_BACKEND_URL: ${{secrets.NEXT_PUBLIC_BACKEND_URL_DEV_DS}} + BACKEND_GRAPHQL_URL: ${{secrets.BACKEND_GRAPHQL_URL_DEV_DS}} + NEXT_PUBLIC_ENABLE_ACCESSMODEL: ${{secrets.NEXT_PUBLIC_ENABLE_ACCESSMODEL_DS}} + NEXT_PUBLIC_BACKEND_GRAPHQL_URL: ${{secrets.NEXT_PUBLIC_BACKEND_GRAPHQL_URL_DEV_DS}} + BACKEND_URL: ${{secrets.BACKEND_URL_DEV}} + NEXT_PUBLIC_PLATFORM_URL: ${{secrets.NEXT_PUBLIC_PLATFORM_URL_DEV}} + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: npm install + + - name: Generate + run: npm run generate + + - name: Build + run: npm run build + + - name: Rename .next to .next2 + run: mv .next .next2 + + - name: Rename public to public2 + run: mv public public2 + + - name: Send .next2 to EC2 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.EC2_HOST_DEV_DS }} + username: ${{ secrets.EC2_USERNAME_DS }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + source: .next2 + target: DataExchange/DataExFrontend + + - name: Send public2 to EC2 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.EC2_HOST_DEV_DS }} + username: ${{ secrets.EC2_USERNAME_DS }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + source: public2 + target: DataExchange/DataExFrontend + + - name: Update with new Build + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_HOST_DEV_DS }} + username: ${{ secrets.EC2_USERNAME_DS }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + script: rm -rf DataExchange/DataExFrontend/.next; rm -rf DataExchange/DataExFrontend/public; mv DataExchange/DataExFrontend/.next2 DataExchange/DataExFrontend/.next; mv DataExchange/DataExFrontend/public2 DataExchange/DataExFrontend/public; /home/ubuntu/.nvm/versions/node/v20.11.1/bin/pm2 restart dataspace diff --git a/.github/workflows/deploy-Dataspace.yml b/.github/workflows/deploy-Dataspace.yml new file mode 100644 index 00000000..05999d98 --- /dev/null +++ b/.github/workflows/deploy-Dataspace.yml @@ -0,0 +1,69 @@ +name: Update DataSpace Prod + +on: + push: + branches: ['main'] +env: + KEYCLOAK_CLIENT_ID: ${{secrets.KEYCLOAK_CLIENT_ID}} + KEYCLOAK_CLIENT_SECRET: ${{secrets.KEYCLOAK_CLIENT_SECRET}} + AUTH_ISSUER: ${{secrets.AUTH_ISSUER}} + NEXTAUTH_URL: ${{secrets.NEXTAUTH_URL_DS}} + NEXT_PUBLIC_NEXTAUTH_URL: ${{secrets.NEXT_PUBLIC_NEXTAUTH_URL_DS}} + NEXTAUTH_SECRET: ${{secrets.NEXTAUTH_SECRET}} + END_SESSION_URL: ${{secrets.END_SESSION_URL}} + REFRESH_TOKEN_URL: ${{secrets.REFRESH_TOKEN_URL}} + NEXT_PUBLIC_BACKEND_URL: ${{secrets.NEXT_PUBLIC_BACKEND_URL_DS}} + BACKEND_GRAPHQL_URL: ${{secrets.BACKEND_GRAPHQL_URL_DS}} + NEXT_PUBLIC_ENABLE_ACCESSMODEL: ${{secrets.NEXT_PUBLIC_ENABLE_ACCESSMODEL_DS}} + NEXT_PUBLIC_BACKEND_GRAPHQL_URL: ${{secrets.NEXT_PUBLIC_BACKEND_GRAPHQL_URL_DS}} + BACKEND_URL: ${{secrets.BACKEND_URL}} + NEXT_PUBLIC_PLATFORM_URL: ${{secrets.NEXT_PUBLIC_PLATFORM_URL}} + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: npm install + + - name: Generate + run: npm run generate + + - name: Build + run: npm run build + + - name: Rename .next to .next2 + run: mv .next .next2 + + - name: Rename public to public2 + run: mv public public2 + + - name: Send .next2 to EC2 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.EC2_HOST_DS }} + username: ${{ secrets.EC2_USERNAME_DS }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + source: .next2 + target: DataExchange/DataExFrontend + + - name: Send public2 to EC2 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.EC2_HOST_DS }} + username: ${{ secrets.EC2_USERNAME_DS }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + source: public2 + target: DataExchange/DataExFrontend + + - name: Update with new Build + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_HOST_DS }} + username: ${{ secrets.EC2_USERNAME_DS }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + script: rm -rf DataExchange/DataExFrontend/.next; rm -rf DataExchange/DataExFrontend/public; mv DataExchange/DataExFrontend/.next2 DataExchange/DataExFrontend/.next; mv DataExchange/DataExFrontend/public2 DataExchange/DataExFrontend/public; /home/ubuntu/.nvm/versions/node/v20.11.1/bin/pm2 restart dataspace diff --git a/.github/workflows/deploy-IDS-Dataspace.yml b/.github/workflows/deploy-IDS-Dataspace.yml new file mode 100644 index 00000000..804a428c --- /dev/null +++ b/.github/workflows/deploy-IDS-Dataspace.yml @@ -0,0 +1,55 @@ +name: Update IDS-DRR Dataspace + +on: + push: + branches: ['dev'] +env: + KEYCLOAK_CLIENT_ID: ${{secrets.KEYCLOAK_CLIENT_ID}} + KEYCLOAK_CLIENT_SECRET: ${{secrets.KEYCLOAK_CLIENT_SECRET}} + AUTH_ISSUER: ${{secrets.AUTH_ISSUER}} + NEXTAUTH_URL: ${{secrets.NEXTAUTH_URL}} + NEXT_PUBLIC_NEXTAUTH_URL: ${{secrets.NEXT_PUBLIC_NEXTAUTH_URL}} + NEXTAUTH_SECRET: ${{secrets.NEXTAUTH_SECRET}} + END_SESSION_URL: ${{secrets.END_SESSION_URL}} + REFRESH_TOKEN_URL: ${{secrets.REFRESH_TOKEN_URL}} + NEXT_PUBLIC_BACKEND_URL: ${{secrets.NEXT_PUBLIC_BACKEND_URL}} + BACKEND_GRAPHQL_URL: ${{secrets.BACKEND_GRAPHQL_URL}} + NEXT_PUBLIC_ENABLE_ACCESSMODEL: ${{secrets.NEXT_PUBLIC_ENABLE_ACCESSMODEL}} + NEXT_PUBLIC_BACKEND_GRAPHQL_URL: ${{secrets.NEXT_PUBLIC_BACKEND_GRAPHQL_URL}} + BACKEND_URL: ${{secrets.BACKEND_URL}} + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: npm install + + - name: Generate + run: npm run generate + + - name: Build + run: npm run build + + - name: Rename .next to .next2 + run: mv .next .next2 + + - name: Send .next2 to EC2 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + source: .next2 + target: DataExchange/DataExFrontend + - name: Update with new Build + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + script: rm -rf DataExchange/DataExFrontend/.next; mv DataExchange/DataExFrontend/.next2 DataExchange/DataExFrontend/.next; /home/ubuntu/.nvm/versions/node/v18.17.0/bin/pm2 restart DataExFrontend diff --git a/.github/workflows/pre-merge.yml b/.github/workflows/pre-merge.yml index aaedafe0..e245f9b7 100644 --- a/.github/workflows/pre-merge.yml +++ b/.github/workflows/pre-merge.yml @@ -9,12 +9,18 @@ env: KEYCLOAK_CLIENT_ID: ${{secrets.KEYCLOAK_CLIENT_ID}} KEYCLOAK_CLIENT_SECRET: ${{secrets.KEYCLOAK_CLIENT_SECRET}} AUTH_ISSUER: ${{secrets.AUTH_ISSUER}} - NEXTAUTH_URL: ${{secrets.NEXTAUTH_URL}} + NEXTAUTH_URL: ${{secrets.NEXTAUTH_URL_DS}} + NEXT_PUBLIC_NEXTAUTH_URL: ${{secrets.NEXT_PUBLIC_NEXTAUTH_URL_DS}} NEXTAUTH_SECRET: ${{secrets.NEXTAUTH_SECRET}} END_SESSION_URL: ${{secrets.END_SESSION_URL}} REFRESH_TOKEN_URL: ${{secrets.REFRESH_TOKEN_URL}} - NEXT_PUBLIC_BACKEND_URL: ${{secrets.NEXT_PUBLIC_BACKEND_URL}} - BACKEND_GRAPHQL_URL: ${{secrets.BACKEND_GRAPHQL_URL}} + NEXT_PUBLIC_BACKEND_URL: ${{secrets.NEXT_PUBLIC_BACKEND_URL_DS}} + BACKEND_URL: ${{secrets.BACKEND_URL_DS}} + BACKEND_GRAPHQL_URL: ${{secrets.BACKEND_GRAPHQL_URL_DS}} + NEXT_PUBLIC_ENABLE_ACCESSMODEL: ${{secrets.NEXT_PUBLIC_ENABLE_ACCESSMODEL_DS}} + NEXT_PUBLIC_BACKEND_GRAPHQL_URL: ${{secrets.NEXT_PUBLIC_BACKEND_GRAPHQL_URL_DS}} + NEXT_PUBLIC_PLATFORM_URL: ${{secrets.NEXT_PUBLIC_PLATFORM_URL}} + jobs: build: @@ -25,7 +31,7 @@ jobs: node-version: [20.x] env: - BACKEND_GRAPHQL_URL: ${{secrets.BACKEND_GRAPHQL_URL}} + BACKEND_GRAPHQL_URL: ${{secrets.BACKEND_GRAPHQL_URL_DS}} steps: - uses: actions/checkout@v4 diff --git a/app/[locale]/(user)/about-us/components/Initiatives.tsx b/app/[locale]/(user)/about-us/components/Initiatives.tsx new file mode 100644 index 00000000..0f3cba50 --- /dev/null +++ b/app/[locale]/(user)/about-us/components/Initiatives.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import Image from 'next/image'; +import { Divider, Text } from 'opub-ui'; + +const Initiatives = () => { + const IntiativesList = [ + { + name: 'Climate Action', + image: '/About/ca.png', + description: + 'Asia is the world’s most disaster-prone region, with 80% of extreme weather events caused by floods and storms. To help better prepare Asia for climate change, we are actively working with frontline workers, government agencies and other stakeholders to strengthen climate action and disaster risk reduction. These efforts are spread across all our initiatives, including projects like Green budgeting, Disaster Modelling and Citizen-led Disaster Reporting.', + }, + { + name: 'Digital Public Goods', + image: '/About/dpg.png', + description: + 'We are co-creating people-centric Digital Public Goods (DPGs), especially Open Data Platforms, Data Exchanges, Data Science Models and Citizen-led Apps for improving participatory data-driven governance and attain the Sustainable Development Goals (SDGs) and do no harm by adhering to privacy practices and applicable laws. DPGs and Digital Public Infrastructure (DPI) are critical enablers of digital transformation and are helping to improve public service delivery at scale.', + }, + { + name: 'Law and Justice', + image: '/About/l&j.png', + description: + 'To better understand implementation of laws, judicial reforms and attainment of human rights, its essential to track timely data of our courts, police, correctional homes, legal aid and other institutions. We have co-created Justice Hub - a crowdsourcing open data platform to help various stakeholders publish, consume and analyse legal data in India. We are also working to publish and analyse data to improve child protection in the country.', + }, + { + name: 'Open Contracting India', + image: '/About/oci.png', + description: + 'Demystifying public finance helps in understanding government’s fiscal and development priorities, supports equitable public policymaking and enables citizen trust. We are working at the national, sub-national and local level to help publish, standardise and analyse public finance data at Open Budgets India and other platforms, for data-driven decision-making and citizen participation. We also collaborate with various stakeholders to set best practices for green and inclusive budgeting for our sustainable future.', + }, + { + name: 'Public Finance', + image: '/About/pf.png', + description: + 'Demystifying public finance helps in understanding government’s fiscal and development priorities, supports equitable public policymaking and enables citizen trust. We are working at the national, sub-national and local level to help publish, standardise and analyse public finance data at Open Budgets India and other platforms, for data-driven decision-making and citizen participation. We also collaborate with various stakeholders to set best practices for green and inclusive budgeting for our sustainable future.', + }, + { + name: 'Urban Development', + image: '/About/ud.png', + description: + 'With rapid urbanisation, cities are becoming constantly evolving complex clusters, presenting new challenges and opportunities of economic development, infrastructure growth, migration and sustainability. To better understand and shape the development of our cities, we are opening-up crucial urban data and building the capacity of urban governments and local stakeholders. We are co-creating solutions like - city data platforms, citizen-led disaster reporting platforms, data science models for effective urban service delivery and more.', + }, + ]; + return ( +
+
+
+ Our Initiatives +
+
+ {IntiativesList.map((item, index) => ( +
+
+ {'initiative + {item.name} +
+ + {item.description} + +
+
+
+ ))} +
+
+
+ ); +}; + +export default Initiatives; diff --git a/app/[locale]/(user)/about-us/page.tsx b/app/[locale]/(user)/about-us/page.tsx new file mode 100644 index 00000000..6aa7a461 --- /dev/null +++ b/app/[locale]/(user)/about-us/page.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import Image from 'next/image'; +import { Text } from 'opub-ui'; + +import BreadCrumbs from '@/components/BreadCrumbs'; +import Initiatives from './components/Initiatives'; + +const About = () => { + return ( +
+ +
+
+
+ About us + + CivicDataLab (CDL) works at the intersection of data, technology, + design and social science to strengthen access to public + information, evidence-based decision-making and citizen + participation in governance. CDL harnesses the potential of open + knowledge movements to strengthen the data-for-public-good + ecosystem and enable citizens to engage in matters of public + reform. We work closely with governments, non-profits, + think-tanks, media houses and universities to enhance their data + and technology capacity to better data-driven decision-making at + scale. + +
+ {'about-us-illustration'} +
+
+
+
+
+ + Our current areas of expertise include digital public goods & + infrastructure (DPGs & DPI), climate change, public finance, urban + development, open contracting and law & justice. We have + co-created digital public goods like open data platforms, data + exchanges, data science models and citizen-led apps for improving + participatory data-driven governance in India and other countries. + +
+
+ + In the last five years, we have collected, cleaned and published + nearly 30,000+ public interest datasets and are catering to an + active user base of more than a million citizens. Some of our + publicly available open data initiatives include Open Budgets + India, Justice Hub, Open Contracting India, Open City, CogniCity + among others. We have co-created digital public goods with + National Informatics Center (NIC), Ministry of Electronic & + Information Technology (MeitY) and the Government of Assam. + Additionally, we actively build capacity for a diverse group of + partners working to enhance social impact, situated in India, + Indonesia, Philippines, Thailand, Panama and Scotland. + +
+
+
+
+ +
+
+ ); +}; + +export default About; diff --git a/app/[locale]/(user)/categories/[categorySlug]/page.tsx b/app/[locale]/(user)/categories/[categorySlug]/page.tsx deleted file mode 100644 index f0644e48..00000000 --- a/app/[locale]/(user)/categories/[categorySlug]/page.tsx +++ /dev/null @@ -1,396 +0,0 @@ -'use client'; - -import GraphqlPagination from '@/app/[locale]/dashboard/components/GraphqlPagination/graphqlPagination'; -import { fetchDatasets } from '@/fetch'; -import { graphql } from '@/gql'; -import { useQuery } from '@tanstack/react-query'; -import Image from 'next/image'; -import { useRouter } from 'next/navigation'; -import { Pill, SearchInput, Select, Text } from 'opub-ui'; -import { useEffect, useReducer, useState } from 'react'; - -import BreadCrumbs from '@/components/BreadCrumbs'; -import { ErrorPage } from '@/components/error'; -import { Loading } from '@/components/loading'; -import { GraphQL } from '@/lib/api'; -import Card from '../../datasets/components/Card'; -import Filter from '../../datasets/components/FIlter/Filter'; - -const categoryQueryDoc: any = graphql(` - query CategoryDetails($filters: CategoryFilter) { - categories(filters: $filters) { - id - name - description - datasetCount - } - } -`); - -interface Bucket { - key: string; - doc_count: number; -} - -interface Aggregation { - buckets: Bucket[]; -} - -interface Aggregations { - [key: string]: Aggregation; -} - -interface FilterOptions { - [key: string]: string[]; -} - -interface QueryParams { - pageSize: number; - currentPage: number; - filters: FilterOptions; - query?: string; -} - -type Action = - | { type: 'SET_PAGE_SIZE'; payload: number } - | { type: 'SET_CURRENT_PAGE'; payload: number } - | { type: 'SET_FILTERS'; payload: { category: string; values: string[] } } - | { type: 'REMOVE_FILTER'; payload: { category: string; value: string } } - | { type: 'SET_QUERY'; payload: string } - | { type: 'INITIALIZE'; payload: QueryParams }; - -const initialState: QueryParams = { - pageSize: 5, - currentPage: 1, - filters: {}, - query: '', -}; - -const queryReducer = (state: QueryParams, action: Action): QueryParams => { - switch (action.type) { - case 'SET_PAGE_SIZE': { - return { ...state, pageSize: action.payload, currentPage: 1 }; - } - case 'SET_CURRENT_PAGE': { - return { ...state, currentPage: action.payload }; - } - case 'SET_FILTERS': { - return { - ...state, - filters: { - ...state.filters, - [action.payload.category]: action.payload.values, - }, - currentPage: 1, - }; - } - case 'REMOVE_FILTER': { - const newFilters = { ...state.filters }; - newFilters[action.payload.category] = newFilters[ - action.payload.category - ].filter((v) => v !== action.payload.value); - return { ...state, filters: newFilters, currentPage: 1 }; - } - case 'SET_QUERY': { - return { ...state, query: action.payload }; - } - case 'INITIALIZE': { - return { ...state, ...action.payload }; - } - default: - return state; - } -}; - -const useUrlParams = ( - queryParams: QueryParams, - setQueryParams: React.Dispatch, - setVariables: (vars: string) => void -) => { - const router = useRouter(); - - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const sizeParam = urlParams.get('size'); - const pageParam = urlParams.get('page'); - const filters: FilterOptions = {}; - - urlParams.forEach((value, key) => { - if (!['size', 'page', 'query'].includes(key)) { - filters[key] = value.split(','); - } - }); - - const initialParams: QueryParams = { - pageSize: sizeParam ? Number(sizeParam) : 5, - currentPage: pageParam ? Number(pageParam) : 1, - filters, - query: urlParams.get('query') || '', - }; - - setQueryParams({ type: 'INITIALIZE', payload: initialParams }); - }, [setQueryParams]); - - useEffect(() => { - const filtersString = Object.entries(queryParams.filters) - .filter(([_, values]) => values.length > 0) - .map(([key, values]) => `${key}=${values.join(',')}`) - .join('&'); - - const searchParam = queryParams.query - ? `&query=${encodeURIComponent(queryParams.query)}` - : ''; - const variablesString = `?${filtersString}&size=${queryParams.pageSize}&page=${queryParams.currentPage}${searchParam}`; - setVariables(variablesString); - - const currentUrl = new URL(window.location.href); - currentUrl.searchParams.set('size', queryParams.pageSize.toString()); - currentUrl.searchParams.set('page', queryParams.currentPage.toString()); - - Object.entries(queryParams.filters).forEach(([key, values]) => { - if (values.length > 0) { - currentUrl.searchParams.set(key, values.join(',')); - } else { - currentUrl.searchParams.delete(key); - } - }); - - if (queryParams.query) { - currentUrl.searchParams.set('query', queryParams.query); - } else { - currentUrl.searchParams.delete('query'); - } - - router.push(currentUrl.toString()); - }, [queryParams, setVariables, router]); -}; - -const CategoryDetailsPage = ({ params }: { params: { categorySlug: any } }) => { - const getCategoryDetails: { - data: any; - isLoading: boolean; - isError: boolean; - } = useQuery([`get_category_details_${params.categorySlug}`], () => - GraphQL(categoryQueryDoc, { filters: { slug: params.categorySlug } }) - ); - - const [facets, setFacets] = useState<{ - results: any[]; - total: number; - aggregations: Aggregations; - } | null>(null); - const [variables, setVariables] = useState(''); - const [open, setOpen] = useState(false); - const count = facets?.total ?? 0; - const datasetDetails = facets?.results ?? []; - const [queryParams, setQueryParams] = useReducer(queryReducer, initialState); - - useEffect(() => { - if (variables) { - fetchDatasets(variables) - .then((res) => { - setFacets(res); - }) - .catch((err) => { - console.error(err); - }); - } - }, [variables]); - - useUrlParams(queryParams, setQueryParams, setVariables); - - const handlePageChange = (newPage: number) => { - setQueryParams({ type: 'SET_CURRENT_PAGE', payload: newPage }); - }; - - const handlePageSizeChange = (newSize: number) => { - setQueryParams({ type: 'SET_PAGE_SIZE', payload: newSize }); - }; - - const handleFilterChange = (category: string, values: string[]) => { - setQueryParams({ type: 'SET_FILTERS', payload: { category, values } }); - }; - - const handleRemoveFilter = (category: string, value: string) => { - setQueryParams({ type: 'REMOVE_FILTER', payload: { category, value } }); - }; - - const handleSearch = (searchTerm: string) => { - setQueryParams({ type: 'SET_QUERY', payload: searchTerm }); - }; - - const aggregations: Aggregations = facets?.aggregations || {}; - - const filterOptions = Object.entries(aggregations).reduce( - (acc: Record, [key, value]) => { - acc[key.replace('.raw', '')] = value.buckets.map((bucket: Bucket) => ({ - label: bucket.key, - value: bucket.key, - })); - return acc; - }, - {} - ); - - return ( -
- - - {getCategoryDetails.isError ? ( - - ) : getCategoryDetails.isLoading ? ( - - ) : ( -
-
-
- {`${params.categorySlug} -
-
- - {getCategoryDetails.data?.categories[0].name || - params.categorySlug} - - - {getCategoryDetails.data?.categories[0].datasetCount} Datasets - - - {getCategoryDetails.data?.categories[0].description || - 'No description available.'} - -
-
- -
-
-
- Showing 10 of 30 Datasets -
-
- console.log(value)} - onClear={(value: any) => console.log(value)} - /> -
-
-
- - Sort by: - - -
-
-
-
- -
-
- -
-
-
- {Object.entries(queryParams.filters).map(([category, values]) => - values.map((value) => ( - handleRemoveFilter(category, value)} - > - {value} - - )) - )} -
- -
- {facets && datasetDetails?.length > 0 && ( - - {datasetDetails.map((item: any, index: any) => ( - - ))} - - )} -
-
-
-
- )} -
- ); -}; - -export default CategoryDetailsPage; diff --git a/app/[locale]/(user)/categories/page.tsx b/app/[locale]/(user)/categories/page.tsx deleted file mode 100644 index cebb9a29..00000000 --- a/app/[locale]/(user)/categories/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -'use client'; - -import { graphql } from '@/gql'; -import { useQuery } from '@tanstack/react-query'; -import Image from 'next/image'; -import Link from 'next/link'; -import { Text } from 'opub-ui'; - -import BreadCrumbs from '@/components/BreadCrumbs'; -import { ErrorPage } from '@/components/error'; -import { Loading } from '@/components/loading'; -import { GraphQL } from '@/lib/api'; - -const categoriesListQueryDoc: any = graphql(` - query CategoriesList { - categories { - id - name - description - slug - datasetCount - } - } -`); - -const CategoriesListingPage = () => { - const getCategoriesList: { - data: any; - isLoading: boolean; - error: any; - isError: boolean; - } = useQuery([`categories_list_page`], () => - GraphQL(categoriesListQueryDoc, []) - ); - - return ( -
- - <> - {getCategoriesList.isLoading ? ( - - ) : getCategoriesList.data?.categories.length > 0 ? ( - <> -
- - Categories - -
- {getCategoriesList.data?.categories.map((category: any) => ( - -
-
- {'Category -
-
- - {category.name} - - {category.datasetCount} Dataset(s) -
-
- - ))} -
-
- - ) : getCategoriesList.isError ? ( - - ) : ( - <> - )} - -
- ); -}; - -export default CategoriesListingPage; diff --git a/app/[locale]/(user)/components/Content.tsx b/app/[locale]/(user)/components/Content.tsx index 446e7692..1cee3cd8 100644 --- a/app/[locale]/(user)/components/Content.tsx +++ b/app/[locale]/(user)/components/Content.tsx @@ -1,44 +1,155 @@ 'use client'; -import { IconBrandTabler } from '@tabler/icons-react'; -import { useTranslations } from 'next-intl'; -import { Button, ButtonGroup, Icon, Text } from 'opub-ui'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { graphql } from '@/gql'; +import { useQuery } from '@tanstack/react-query'; +import { SearchInput, Spinner, Tag, Text } from 'opub-ui'; -import { Icons } from '@/components/icons'; +import { GraphQL } from '@/lib/api'; +import { cn } from '@/lib/utils'; +import Styles from '../page.module.scss'; -export function Content() { - const t = useTranslations('home'); +const statsInfo: any = graphql(` + query StatsList { + stats { + totalUsers + totalPublishedDatasets + totalPublishers + totalPublishedUsecases + } + } +`); +// const tagsInfo: any = graphql(` +// query TagsData { +// tags { +// id +// value +// } +// } +// `); + +export const Content = () => { + const router = useRouter(); + const Stats: { data: any; isLoading: any } = useQuery([`statsDetails`], () => + GraphQL(statsInfo, {}, []) + ); + // const Tags: { data: any; isLoading: any } = useQuery([`tagDetails`], () => + // GraphQL(tagsInfo, {}, []) + // ); + + const handleSearch = (value: string) => { + if (value) { + router.push(`/datasets?query=${encodeURIComponent(value)}`); + } + }; + const Metrics = [ + { + label: 'Datasets', + count: Stats?.data?.stats?.totalPublishedDatasets, + }, + { + label: 'Use Cases', + count: Stats?.data?.stats?.totalPublishedUsecases, + }, + + { + label: 'Publishers', + count: Stats?.data?.stats?.totalPublishers, + }, + { + label: 'Users', + count: Stats?.data?.stats?.totalUsers, + }, + ]; + + const Sectors = [ + 'Budgets', + 'Child Rights', + 'Disaster Risk Reduction', + 'Climate Finance', + 'Law And Justice', + 'Urban Development', + ]; return ( - <> - - - {t('title')} - - - {t('subtitle')} - - - - - - +
+
+
+
+ + Collaborate to advance + + + Data-driven Impact and Action + + + with CivicDataLab{' '} + +
+ {Stats.isLoading ? ( +
+ +
+ ) : ( +
+ {Metrics.map((item, index) => ( +
+ + {item.count} + + + {item.label} + +
+ ))} +
+ )} +
+ +
+
+ {Sectors.map((item, index) => ( +
+ + + + {item} + + + +
+ ))} +
+
+
+ illustration +
+
+
); -} +}; diff --git a/app/[locale]/(user)/components/Datasets.tsx b/app/[locale]/(user)/components/Datasets.tsx new file mode 100644 index 00000000..f570e786 --- /dev/null +++ b/app/[locale]/(user)/components/Datasets.tsx @@ -0,0 +1,146 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { fetchDatasets } from '@/fetch'; +import { + Button, + Card, + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, + Spinner, + Text, +} from 'opub-ui'; + +import { cn } from '@/lib/utils'; +import { Icons } from '@/components/icons'; +import Styles from './datasets.module.scss'; + +interface Bucket { + key: string; + doc_count: number; +} +interface Aggregation { + buckets: Bucket[]; +} + +interface Aggregations { + [key: string]: Aggregation; +} + +const Datasets = () => { + const [facets, setFacets] = useState<{ + results: any[]; + total: number; + aggregations: Aggregations; + } | null>(null); + const [isLoading, setIsLoading] = useState(true); + useEffect(() => { + fetchDatasets('?sort=recent&size=5&page=1&sort=recent') + .then((res) => { + setFacets(res); + setIsLoading(false); + }) + .catch((err) => { + console.error(err); + }); + }, []); + + const router = useRouter(); + + return ( +
+
+ Popular Datasets +
+ + The most popular Datasets on CivicDataSpace{' '} + + +
+
+
+ + + + + {isLoading ? ( +
+ +
+ ) : ( + facets && + facets.results.map((item: any) => ( + + {' '} + + + )) + )} +
+ +
+
+
+ ); +}; + +export default Datasets; diff --git a/app/[locale]/(user)/components/ListingComponent.tsx b/app/[locale]/(user)/components/ListingComponent.tsx new file mode 100644 index 00000000..307f99a3 --- /dev/null +++ b/app/[locale]/(user)/components/ListingComponent.tsx @@ -0,0 +1,532 @@ +import React, { useEffect, useReducer, useState } from 'react'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; +import GraphqlPagination from '@/app/[locale]/dashboard/components/GraphqlPagination/graphqlPagination'; +import { + Button, + ButtonGroup, + Card, + Icon, + Pill, + SearchInput, + Select, + Text, + Tray, +} from 'opub-ui'; + +import { cn, formatDate } from '@/lib/utils'; +import BreadCrumbs from '@/components/BreadCrumbs'; +import { Icons } from '@/components/icons'; +import { Loading } from '@/components/loading'; +import Filter from '../datasets/components/FIlter/Filter'; +import Styles from '../datasets/dataset.module.scss'; + +// Interfaces +interface Bucket { + key: string; + doc_count: number; +} + +interface Aggregation { + buckets: Bucket[]; +} + +interface Aggregations { + [key: string]: Aggregation; +} + +interface FilterOptions { + [key: string]: string[]; +} + +interface QueryParams { + pageSize: number; + currentPage: number; + filters: FilterOptions; + query?: string; + sort?: string; + order?: string; +} + +type Action = + | { type: 'SET_PAGE_SIZE'; payload: number } + | { type: 'SET_CURRENT_PAGE'; payload: number } + | { type: 'SET_FILTERS'; payload: { category: string; values: string[] } } + | { type: 'REMOVE_FILTER'; payload: { category: string; value: string } } + | { type: 'SET_QUERY'; payload: string } + | { type: 'SET_SORT'; payload: string } + | { type: 'SET_ORDER'; payload: string } + | { type: 'INITIALIZE'; payload: QueryParams }; + +// Initial State +const initialState: QueryParams = { + pageSize: 9, + currentPage: 1, + filters: {}, + query: '', + sort: 'recent', + order: '', +}; + +// Query Reducer +const queryReducer = (state: QueryParams, action: Action): QueryParams => { + switch (action.type) { + case 'SET_PAGE_SIZE': + return { ...state, pageSize: action.payload, currentPage: 1 }; + case 'SET_CURRENT_PAGE': + return { ...state, currentPage: action.payload }; + case 'SET_FILTERS': + return { + ...state, + filters: { + ...state.filters, + [action.payload.category]: action.payload.values, + }, + currentPage: 1, + }; + case 'REMOVE_FILTER': { + const newFilters = { ...state.filters }; + newFilters[action.payload.category] = newFilters[ + action.payload.category + ].filter((v) => v !== action.payload.value); + return { ...state, filters: newFilters, currentPage: 1 }; + } + case 'SET_QUERY': + return { ...state, query: action.payload, currentPage: 1 }; + case 'SET_SORT': + return { ...state, sort: action.payload }; + case 'SET_ORDER': + return { ...state, order: action.payload }; + case 'INITIALIZE': + return { ...state, ...action.payload }; + default: + return state; + } +}; + +// URL Params Hook +const useUrlParams = ( + queryParams: QueryParams, + setQueryParams: React.Dispatch, + setVariables: (vars: string) => void +) => { + const router = useRouter(); + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const sizeParam = urlParams.get('size'); + const pageParam = urlParams.get('page'); + const filters: FilterOptions = {}; + + urlParams.forEach((value, key) => { + if (!['size', 'page', 'query'].includes(key)) { + filters[key] = value.split(','); + } + }); + + const initialParams: QueryParams = { + pageSize: sizeParam ? Number(sizeParam) : 9, + currentPage: pageParam ? Number(pageParam) : 1, + filters, + query: urlParams.get('query') || '', + }; + + setQueryParams({ type: 'INITIALIZE', payload: initialParams }); + }, [setQueryParams]); + + useEffect(() => { + const filtersString = Object.entries(queryParams.filters) + .filter(([_, values]) => values.length > 0) + .map(([key, values]) => `${key}=${values.join(',')}`) + .join('&'); + + const searchParam = queryParams.query + ? `&query=${encodeURIComponent(queryParams.query)}` + : ''; + const sortParam = queryParams.sort + ? `&sort=${encodeURIComponent(queryParams.sort)}` + : ''; + const orderParam = queryParams.order + ? `&order=${encodeURIComponent(queryParams.order)}` + : ''; + const variablesString = `?${filtersString}&size=${queryParams.pageSize}&page=${queryParams.currentPage}${searchParam}${sortParam}${orderParam}`; + setVariables(variablesString); + + const currentUrl = new URL(window.location.href); + currentUrl.searchParams.set('size', queryParams.pageSize.toString()); + currentUrl.searchParams.set('page', queryParams.currentPage.toString()); + + Object.entries(queryParams.filters).forEach(([key, values]) => { + if (values.length > 0) { + currentUrl.searchParams.set(key, values.join(',')); + } else { + currentUrl.searchParams.delete(key); + } + }); + + if (queryParams.query) { + currentUrl.searchParams.set('query', queryParams.query); + } else { + currentUrl.searchParams.delete('query'); + } + if (queryParams.sort) { + currentUrl.searchParams.set('sort', queryParams.sort); + } else { + currentUrl.searchParams.delete('sort'); + } + if (queryParams.order) { + currentUrl.searchParams.set('order', queryParams.order); + } else { + currentUrl.searchParams.delete('order'); + } + router.replace(currentUrl.toString()); + }, [queryParams, setVariables, router]); +}; + +// Listing Component Props +interface ListingProps { + fetchDatasets: (variables: string) => Promise<{ + results: any[]; + total: number; + aggregations: Aggregations; + }>; + breadcrumbData: { href: string; label: string }[]; + headerComponent?: React.ReactNode; + categoryName?: string; + categoryDescription?: string; + categoryImage?: string; +} + +const ListingComponent: React.FC = ({ + fetchDatasets, + breadcrumbData, + headerComponent, + categoryName, + categoryDescription, + categoryImage, +}) => { + const [facets, setFacets] = useState<{ + results: any[]; + total: number; + aggregations: Aggregations; + } | null>(null); + const [variables, setVariables] = useState(''); + const [open, setOpen] = useState(false); + const [queryParams, setQueryParams] = useReducer(queryReducer, initialState); + const [view, setView] = useState<'collapsed' | 'expanded'>('collapsed'); + + const count = facets?.total ?? 0; + const datasetDetails = facets?.results ?? []; + + useUrlParams(queryParams, setQueryParams, setVariables); + + useEffect(() => { + if (variables) { + fetchDatasets(variables) + .then((res) => { + setFacets(res); + }) + .catch((err) => { + console.error(err); + }); + } + }, [variables, fetchDatasets]); + + const [hasMounted, setHasMounted] = useState(false); + + useEffect(() => { + setHasMounted(true); + }, []); + + if (!hasMounted) return ; + + const handlePageChange = (newPage: number) => { + setQueryParams({ type: 'SET_CURRENT_PAGE', payload: newPage }); + }; + + const handlePageSizeChange = (newSize: number) => { + setQueryParams({ type: 'SET_PAGE_SIZE', payload: newSize }); + }; + + const handleFilterChange = (category: string, values: string[]) => { + setQueryParams({ type: 'SET_FILTERS', payload: { category, values } }); + }; + + const handleRemoveFilter = (category: string, value: string) => { + setQueryParams({ type: 'REMOVE_FILTER', payload: { category, value } }); + }; + + const handleSearch = (searchTerm: string) => { + setQueryParams({ type: 'SET_QUERY', payload: searchTerm }); + }; + + const handleSortChange = (sortOption: string) => { + setQueryParams({ type: 'SET_SORT', payload: sortOption }); + }; + + const handleOrderChange = (sortOrder: string) => { + setQueryParams({ type: 'SET_ORDER', payload: sortOrder }); + }; + + const aggregations: Aggregations = facets?.aggregations || {}; + + const filterOptions = Object.entries(aggregations).reduce( + (acc: Record, [key, value]) => { + acc[key] = Object.entries(value).map(([bucketKey]) => ({ + label: bucketKey, + value: bucketKey, + })); + return acc; + }, + {} + ); + + return ( +
+ +
+ {/* Optional Category Header */} + {(categoryName || categoryDescription || categoryImage) && ( +
+ {categoryImage && ( +
+ {`${categoryName} +
+ )} +
+ {categoryName && ( + + {categoryName} + + )} + + + {categoryDescription + ? categoryDescription + : 'No Description Provided'} + +
+
+ )} + + {/* Optional Header Component */} + {headerComponent} + +
+
+
+ +
+ +
+
+
+ handleSearch(value)} + onClear={(value) => handleSearch(value)} + /> +
+
+
+ + + + +
+
+ +
+
+ -
- setOpen(true)} - > - Filter - - } - > - - -
-
-
- -
- -
-
- {Object.entries(queryParams.filters).map(([category, values]) => - values.map((value) => ( - handleRemoveFilter(category, value)} - > - {value} - - )) - )} -
- -
- {facets && datasetDetails?.length > 0 && ( - - {datasetDetails.map((item: any, index: any) => ( - - ))} - - )} -
-
-
- - )} - + ); }; -export default DatasetsListing; +export default DatasetsListing; \ No newline at end of file diff --git a/app/[locale]/(user)/form/page.tsx b/app/[locale]/(user)/form/page.tsx deleted file mode 100644 index 187a6b11..00000000 --- a/app/[locale]/(user)/form/page.tsx +++ /dev/null @@ -1,196 +0,0 @@ -'use client'; - -import React from 'react'; -import { - Button, - Checkbox, - CheckboxGroup, - Combobox, - DateField, - DatePicker, - DateRangePicker, - Form, - FormLayout, - Input, - MonthPicker, - RadioGroup, - RadioItem, - RangeSlider, - Select, - Text, - TimeField, -} from 'opub-ui'; - -const options = [ - { label: 'Today', value: 'today' }, - { label: 'Yesterday', value: 'yesterday' }, - { label: 'Last 7 days', value: 'lastWeek' }, -]; - -const checkboxOptions = [ - { - label: 'ReactJs', - value: 'react', - helpText: 'Kinda Popular these days.', - }, - { - label: 'VueJs', - value: 'vue', - }, - { - label: 'AngularJs', - value: 'angular', - }, -]; - -const comboboxOptions: { - value: string; - label: string; -}[] = [ - { - value: 'Apple', - label: 'Apple', - }, - { - value: 'Banana', - label: 'Banana', - }, - { - value: 'Burger', - label: 'Burger', - }, - { - value: 'Cherry', - label: 'Cherry', - }, - { - value: 'Grapes', - label: 'Grapes', - }, - { - value: 'Mango', - label: 'Mango', - }, - { - value: 'Orange', - label: 'Orange', - }, - { - value: 'Pineapple', - label: 'Pineapple', - }, - { - value: 'Strawberry', - label: 'Strawberry', - }, - { - value: 'Watermelon', - label: 'Watermelon', - }, -]; - -const defaultValBase = { - text: 'Excalibur', - select: 'yesterday', - range: [6], - checkbox: true, - 'checkbox-group': ['angular', 'vue'], - radio: '1', - date: '2020-02-06', - 'date-picker': '1998-03-25', - 'date-range': { - start: '2020-02-06', - end: '2020-02-10', - }, - time: '16:45:00', - month: '2020-02-01', - combobox: 'Apple', - comboboxMulti: [ - { - value: 'Apple', - label: 'Apple', - }, - { - value: 'Banana', - label: 'Banana', - }, - { - value: 'Burger', - label: 'Burger', - }, - ], -}; - -export default function Page() { - const [values, setValues] = React.useState(); - - return ( -
- <> -
{ - setValues(e); - console.log(e); - }} - formOptions={{ defaultValues: defaultValBase }} - > - - - { + handleSortChange(e); + }} + /> +
+
+
+ {isLoading ? ( +
+ +
+ ) : data && data?.sectors?.length > 0 ? ( + <> +
+ {data?.sectors.map((sectors: any) => ( + +
+
+ {'Sectors +
+
+
+ + {sectors.name} + + +
+
+ + {sectors.datasetCount} + + Datasets +
+
+
+ + ))} +
+ + ) : isError ? ( + + ) : ( + <> + )} +
+
+ + + + ); +}; + +export default SectorsListingPage; diff --git a/app/[locale]/(user)/usecases/[useCaseSlug]/page.tsx b/app/[locale]/(user)/usecases/[useCaseSlug]/page.tsx new file mode 100644 index 00000000..f0fca2b5 --- /dev/null +++ b/app/[locale]/(user)/usecases/[useCaseSlug]/page.tsx @@ -0,0 +1,342 @@ +'use client'; + +import Image from 'next/image'; +import { useParams } from 'next/navigation'; +import { graphql } from '@/gql'; +import { TypeDataset, TypeUseCase } from '@/gql/generated/graphql'; +import { useQuery } from '@tanstack/react-query'; +import { Card, Text } from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { formatDate } from '@/lib/utils'; +import BreadCrumbs from '@/components/BreadCrumbs'; +import { Icons } from '@/components/icons'; +import { Loading } from '@/components/loading'; +import PrimaryDetails from '../components/Details'; +import Metadata from '../components/Metadata'; + +const UseCasedetails: any = graphql(` + query UseCasedetails($pk: ID!) { + useCase(pk: $pk) { + id + title + summary + isIndividualUsecase + user { + fullName + email + profilePicture { + url + } + } + organization { + name + contactEmail + logo { + url + } + } + website + metadata { + metadataItem { + id + label + dataType + } + id + value + } + sectors { + id + name + } + runningStatus + tags { + id + value + } + publishers { + name + contactEmail + logo { + url + } + } + logo { + name + path + } + datasets { + title + id + isIndividualDataset + user { + fullName + id + profilePicture { + url + } + } + downloadCount + description + organization { + name + logo { + url + } + } + metadata { + metadataItem { + id + label + dataType + } + id + value + } + sectors { + name + } + modified + } + contactEmail + status + slug + modified + contributors { + id + fullName + profilePicture { + url + } + } + supportingOrganizations { + id + name + logo { + url + } + } + partnerOrganizations { + id + name + logo { + url + } + } + } + } +`); + +const UseCaseDetailPage = () => { + const params = useParams(); + const { + data: UseCaseDetails, + isLoading, + refetch, + } = useQuery<{ useCase: TypeUseCase }>( + [`fetch_UsecaseDetails_${params.useCaseSlug}`], + () => + GraphQL( + UseCasedetails, + {}, + { + pk: params.useCaseSlug, + } + ), + { + refetchOnMount: true, + refetchOnReconnect: true, + } + ); + const datasets = UseCaseDetails?.useCase?.datasets || []; // Fallback to an empty array + + const hasSupportingOrganizations = + UseCaseDetails?.useCase?.supportingOrganizations && + UseCaseDetails?.useCase?.supportingOrganizations?.length > 0; + const hasPartnerOrganizations = + UseCaseDetails?.useCase?.partnerOrganizations && + UseCaseDetails?.useCase?.partnerOrganizations?.length > 0; + const hasContributors = + UseCaseDetails?.useCase?.contributors && + UseCaseDetails?.useCase?.contributors?.length > 0; + console.log(UseCaseDetails); + + return ( +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + +
+
+
+ +
+
+ +
+
+
+
+ Datasets in this Use Case + + All Datasets related to this Use Case + +
+
+ {datasets.length > 0 && + datasets.map((dataset: TypeDataset) => ( + + meta.metadataItem?.label === 'Geography' + )?.value || '', + }, + ]} + href={`/datasets/${dataset.id}`} + footerContent={[ + { + icon: `/Sectors/${dataset.sectors[0]?.name}.svg`, + label: 'Sectors', + }, + { + icon: dataset.isIndividualDataset + ? dataset?.user?.profilePicture + ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${dataset.user.profilePicture.url}` + : '/profile.png' + : dataset?.organization?.logo + ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${dataset.organization.logo.url}` + : '/org.png', + label: 'Published by', + }, + ]} + description={dataset.description || ''} + /> + ))} +
+
+
+ {(hasSupportingOrganizations || + hasPartnerOrganizations || + hasContributors) && ( +
+
+ {hasSupportingOrganizations && ( +
+ + Supported by + +
+ {UseCaseDetails?.useCase?.supportingOrganizations?.map( + (org: any) => ( +
+ {org.name} +
+ ) + )} +
+
+ )} + {hasPartnerOrganizations && ( +
+ + Partnered by + +
+ {UseCaseDetails?.useCase?.partnerOrganizations?.map( + (org: any) => ( +
+ {org.name} +
+ ) + )} +
+
+ )} +
+ {hasContributors && ( +
+
+ + Contributors{' '} + + + Publisher and Contributors who have added to the Use Case + +
+
+ {UseCaseDetails?.useCase?.contributors?.map( + (contributor: any) => ( + {contributor.fullName} + ) + )} +
+
+ )} +
+ )} + + )} +
+ ); +}; + +export default UseCaseDetailPage; diff --git a/app/[locale]/(user)/usecases/components/Details.tsx b/app/[locale]/(user)/usecases/components/Details.tsx new file mode 100644 index 00000000..8ed2b916 --- /dev/null +++ b/app/[locale]/(user)/usecases/components/Details.tsx @@ -0,0 +1,92 @@ +'use client'; + +import React, { useState } from 'react'; +import Image from 'next/image'; +import { Button, Icon, Spinner, Tag, Text, Tray } from 'opub-ui'; + +import { Icons } from '@/components/icons'; +import Metadata from './Metadata'; + +const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => { + const [open, setOpen] = useState(false); + + return ( +
+
+ {data.useCase.title} +
+
+ {data.useCase.tags.map((item: any, index: number) => ( +
+ {item.value} +
+ ))} +
+
+ + +
+ } + > + {isLoading ? ( +
+ +
+ ) : ( + + )} + +
+
+ {data.useCase.title} +
+
+
+ GEOGRAPHIES +
+ + { + data.useCase.metadata?.find( + (meta: any) => meta.metadataItem?.label === 'Geography' + )?.value + } + +
+
+
+ Summary +
+ + {data.useCase.summary} + +
+
+
+
+ ); +}; + +export default PrimaryDetails; diff --git a/app/[locale]/(user)/usecases/components/Metadata.tsx b/app/[locale]/(user)/usecases/components/Metadata.tsx new file mode 100644 index 00000000..502ab8de --- /dev/null +++ b/app/[locale]/(user)/usecases/components/Metadata.tsx @@ -0,0 +1,154 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { Button, Divider, Icon, Text } from 'opub-ui'; + +import { formatDate } from '@/lib/utils'; +import { Icons } from '@/components/icons'; + +const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => { + const metadata = [ + { + label: data.useCase.isIndividualUsecase ? 'Publisher' : 'Organization', + value: data.useCase.isIndividualUsecase + ? data.useCase.user.fullName + : data?.useCase.organization?.name, + }, + { + label: 'Contact', + value: ( + + Contact{' '} + {data.useCase.isIndividualUsecase ? 'Publisher' : 'Organization'} + + ), + }, + { + label: 'Started On', + value: formatDate(data.useCase.created) || 'N/A', + }, + { + label: 'Status', + value: data.useCase.runningStatus.split('_').join('') || 'N/A', + }, + { + label: 'Last Updated', + value: formatDate(data.useCase.modified) || 'N/A', + }, + { + label: 'Sectors', + value: ( +
+ {data.useCase.sectors.length > 0 ? ( + data.useCase.sectors.map((sector: any, index: number) => ( + {sector.name + )) + ) : ( + N/A // Fallback if no sectors are available + )} +
+ ), + }, + { + label: 'SDG Goals', + value: ( +
+ {data.useCase.metadata.length > 0 ? ( + data.useCase.metadata + ?.find((meta: any) => meta.metadataItem?.label === 'SDG Goal') + ?.value.split(', ') + .map((item: any, index: number) => ( + {item + )) + ) : ( + N/A + )} +
+ ), + }, + ]; + const image = data.useCase.isIndividualUsecase + ? data.useCase?.user?.profilePicture + ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${data.useCase.user.profilePicture.url}` + : '/profile.png' + : data?.useCase.organization?.logo + ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${data.useCase.organization.logo.url}` + : '/org.png'; + + return ( +
+
+
+ + ABOUT THE USECASE{' '} + + METADATA +
+
+ {setOpen && ( + + )} +
+
+ +
+
+ { +
+
+ {metadata.map((item, index) => ( +
+ + {item.label} + + + {typeof item.value === 'string' ? item.value : item.value} + +
+ ))} +
+
+
+ ); +}; + +export default Metadata; diff --git a/app/[locale]/(user)/usecases/page.tsx b/app/[locale]/(user)/usecases/page.tsx new file mode 100644 index 00000000..6d7bf1b8 --- /dev/null +++ b/app/[locale]/(user)/usecases/page.tsx @@ -0,0 +1,181 @@ +'use client'; + +import Image from 'next/image'; +import { graphql } from '@/gql'; +import { useQuery } from '@tanstack/react-query'; +import { Card, Spinner, Text } from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { cn, formatDate } from '@/lib/utils'; +import BreadCrumbs from '@/components/BreadCrumbs'; +import { Icons } from '@/components/icons'; +import { Loading } from '@/components/loading'; +import Styles from '../page.module.scss'; + +const useCasesListQueryDoc: any = graphql(` + query UseCasesList($filters: UseCaseFilter) { + publishedUseCases(filters: $filters) { + id + title + summary + slug + datasetCount + isIndividualUsecase + user { + fullName + profilePicture { + url + } + } + organization { + name + logo { + url + } + } + logo { + path + } + metadata { + metadataItem { + id + label + dataType + } + id + value + } + publishers { + logo { + path + } + name + } + sectors { + id + name + } + created + modified + website + contactEmail + } + } +`); + +const UseCasesListingPage = () => { + const getUseCasesList: { + data: any; + isLoading: boolean; + error: any; + isError: boolean; + } = useQuery([`useCases_list_page`], () => + GraphQL( + useCasesListQueryDoc, + {}, + { + filters: { status: 'PUBLISHED' }, + } + ) + ); + + return ( +
+ +
+
+
+ + Our Use Cases + + + By Use case we mean any specific sector or domain data led + interventions that can be applied to address some of the most + pressing concerns from hyper-local to the global level + simultaneously. + +
+ {'Usecase +
+
+
+
+ Explore Use Cases +
+ {getUseCasesList.isLoading ? ( +
+ +
+ ) : ( +
+ {getUseCasesList && + getUseCasesList?.data?.publishedUseCases.length > 0 && + getUseCasesList?.data?.publishedUseCases.map((item: any, index: any) => ( + meta.metadataItem?.label === 'Geography' + )?.value, + }, + ]} + footerContent={[ + { + icon: `/Sectors/${item?.sectors[0]?.name}.svg`, + label: 'Sectors', + }, + { + icon: item.isIndividualUsecase + ? item?.user?.profilePicture + ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.user.profilePicture.url}` + : '/profile.png' + : item?.organization?.logo + ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.organization.logo.url}` + : '/org.png', + label: 'Published by', + }, + ]} + imageUrl={`${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.logo?.path.replace('/code/files/', '')}`} + description={item.summary} + iconColor="warning" + variation={'collapsed'} + /> + ))} +
+ )} +
+
+ ); +}; + +export default UseCasesListingPage; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/admin/addUser.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/admin/addUser.tsx new file mode 100644 index 00000000..854fb21e --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/admin/addUser.tsx @@ -0,0 +1,273 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import { graphql } from '@/gql'; +import { + AddRemoveUserToOrganizationInput, + AssignOrganizationRoleInput, +} from '@/gql/generated/graphql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { Button, Dialog, Label, Select, toast } from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { toTitleCase } from '@/lib/utils'; +import { FetchUsers } from '../usecases/edit/[id]/contributors/query'; + +const addUserDoc: any = graphql(` + mutation addUserToOrganization($input: AddRemoveUserToOrganizationInput!) { + addUserToOrganization(input: $input) { + __typename + ... on TypeOrganizationMembershipMutationResponse { + data { + role { + name + id + } + } + success + } + } + } +`); +const allRolesDoc: any = graphql(` + query AllRolesDoc { + roles { + id + name + description + } + } +`); + +const updateUser: any = graphql(` + mutation assignOrganizationRole($input: AssignOrganizationRoleInput!) { + assignOrganizationRole(input: $input) { + success + message + } + } +`); + +const AddUser = ({ + setIsOpen, + selectedUser, + isOpen, + isEdit, + setRefetch, +}: { + setIsOpen: (isOpen: boolean) => void; + selectedUser: any; + isOpen: boolean; + isEdit: boolean; + setRefetch: (refetch: boolean) => void; +}) => { + const [searchValue, setSearchValue] = useState(''); + const params = useParams<{ + entityType: string; + entitySlug: string; + id: string; + }>(); + + const Users: { data: any; isLoading: boolean; refetch: any } = useQuery( + [`fetch_users_list`], + () => + GraphQL( + FetchUsers, + {}, + { + limit: 10, + searchTerm: searchValue, + } + ), + { + enabled: searchValue?.length > 0, + keepPreviousData: true, + } + ); + + const RolesList: { data: any; isLoading: boolean; refetch: any } = useQuery( + [`fetch_UseCaseData`], + () => + GraphQL( + allRolesDoc, + { + [params.entityType]: params.entitySlug, + }, + [] + ) + ); + + useEffect(() => { + if (selectedUser) { + setSearchValue(selectedUser.name || ''); + setFormData({ + userId: selectedUser.id || '', + roleId: selectedUser.role?.id || '', + }); + } else { + setFormData({ userId: '', roleId: '' }); + setSearchValue(''); + } + }, [selectedUser]); + + const { mutate, isLoading: addUserLoading } = useMutation( + (input: { input: AddRemoveUserToOrganizationInput }) => + GraphQL( + addUserDoc, + { + [params.entityType]: params.entitySlug, + }, + input + ), + { + onSuccess: (res: any) => { + toast('User added successfully'); + // Optionally, reset form or perform other actions + setIsOpen(false); + setFormData({ + userId: '', + roleId: '', + }); + setRefetch(true); + }, + onError: (err: any) => { + toast('Failed to add user'); + }, + } + ); + + const { mutate: updateMutate, isLoading: updateUserLoading } = useMutation( + (input: { input: AssignOrganizationRoleInput }) => + GraphQL( + updateUser, + { + [params.entityType]: params.entitySlug, + }, + input + ), + { + onSuccess: (res: any) => { + toast('User updated successfully'); + // Optionally, reset form or perform other actions + setIsOpen(false); + setFormData({ + userId: '', + roleId: '', + }); + setRefetch(true); + }, + onError: (err: any) => { + toast('Failed to update user'); + }, + } + ); + + const [formData, setFormData] = useState({ + userId: '', + roleId: '', + }); + const handleChange = (field: string, value: any) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const filteredOptions = Users.data?.searchUsers; + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + if (value === '') { + setFormData({ + userId: '', + roleId: formData.roleId, + }); + } + setSearchValue(value); + setIsDropdownOpen(true); // Keep dropdown open while typing + Users.refetch(); // Refetch when search term changes + }; + + const handleSelectOption = (option: any) => { + handleChange('userId', option.id); + setSearchValue(option.fullName); + setIsDropdownOpen(false); // Close dropdown + }; + + return ( +
+ + {isOpen && ( + +
+
+ + + {isDropdownOpen && filteredOptions?.length > 0 && ( +
+ {filteredOptions.map((option: any) => ( +
handleSelectOption(option)} + > + {option.fullName} +
+ ))} +
+ )} +
+ { + return { + label: item.title, + value: item.id, + }; + } + )} + required + defaultValue={chartData?.dataset?.id} + onChange={(e) => { + if ( + getAllDatasetsWithResourcesRes?.data?.datasets?.find( + (ds: any) => ds.id === e + )?.resources?.length > 0 + ) { + setSelectedDataset(e); + handleSave( + 'resource', + getAllDatasetsWithResourcesRes?.data?.datasets?.find( + (ds: any) => ds.id === e + )?.resources[0].id + ); + } else { + toast.error('No Resources found for this dataset'); + } + }} + /> + + {/* Resource */} + { + handleSave('chartType', e); + }} + /> + + {/* X-axis column */} + { + handleSave('aggregateType', e); + }} + required + /> +
+ ) : ( +
+ { + setXAxisLabelInput(e); + }} + onBlur={() => { + handleSave('xAxisLabel', xAxisLabelInput); + }} + /> + { + setYAxisLabelInput(e); + }} + onBlur={() => { + handleSave('yAxisLabel', yAxisLabelInput); + }} + /> + {chartData.type === 'BAR' && ( +
+ Grouped + { + handleSave('stacked', e); + }} + /> + Stacked +
+ )} + + {chartData.type === 'BAR' && ( + { + setYAxisColumn(e); + }} + /> + + {/* Label for specific element */} + { + setYAxisColumnLabel(e); + }} + /> + + {/* Color for specific element */} + { + setYAxisColumnColor(e); + }} + /> + +
+ + + +
+
+ ); +}; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/[chartID]/components/ChartImagePreview.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/[chartID]/components/ChartImagePreview.tsx new file mode 100644 index 00000000..7bd55170 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/[chartID]/components/ChartImagePreview.tsx @@ -0,0 +1,218 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import { graphql } from '@/gql'; +import { ResourceChartImageInputPartial } from '@/gql/generated/graphql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { Button, Text, toast } from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { Loading } from '@/components/loading'; +import TitleBar from '../../../components/title-bar'; + +const getResourceChartImageDetailsDoc: any = graphql(` + query getResourceChartImageDetails($imageId: UUID!) { + resourceChartImage(imageId: $imageId) { + description + dataset { + id + title + slug + } + id + name + image { + name + path + size + url + width + height + } + status + } + } +`); + +const updateResourceChartImageDoc: any = graphql(` + mutation updateResourceCHartImage($input: ResourceChartImageInputPartial!) { + updateResourceChartImage(input: $input) { + __typename + ... on TypeResourceChartImage { + id + name + } + } + } +`); + +const publishResourceChartImageDoc: any = graphql(` + mutation publishResourceChartImage($resourceChartImageId: UUID!) { + publishResourceChartImage(resourceChartImageId: $resourceChartImageId) + } +`); + +/** + * Renders a page for chart image preview. + * + * @param {{ params: { entityType: string, entitySlug: string, chartID: string } }} props + * @returns {JSX.Element} + */ +const ChartImagePreview = ({ params }: { params: any }) => { + const getResourceChartDetailsRes: { + data: any; + isLoading: boolean; + refetch: any; + error: any; + isError: boolean; + } = useQuery([`getResourceChartImageDetails_${params.chartID}`], () => + GraphQL( + getResourceChartImageDetailsDoc, + { + [params.entityType]: params.entitySlug, + }, + { + imageId: params.chartID, + } + ) + ); + + const [chartTitle, setChartTitle] = useState(''); + + useEffect(() => { + setChartTitle(getResourceChartDetailsRes?.data?.resourceChartImage?.name); + }, [getResourceChartDetailsRes?.data]); + + const updateResourceChartImageMutation: { mutate: any; isLoading: any } = + useMutation( + (data: { input: ResourceChartImageInputPartial }) => + GraphQL( + updateResourceChartImageDoc, + { + [params.entityType]: params.entitySlug, + }, + data + ), + { + onSuccess: () => { + toast('ChartImage Updated Successfully'); + getResourceChartDetailsRes.refetch(); + }, + onError: (err: any) => { + toast(`Received ${err} while updating chart `); + }, + } + ); + + const publishResourceChartImageMutation: { mutate: any; isLoading: any } = + useMutation( + (data: { resourceChartImageId: string }) => + GraphQL( + publishResourceChartImageDoc, + { + [params.entityType]: params.entitySlug, + }, + data + ), + { + onSuccess: () => { + toast('Chart Image Published Successfully'); + getResourceChartDetailsRes.refetch(); + }, + onError: (err: any) => { + toast(`Received ${err} while publishing chart `); + }, + } + ); + + return ( +
+ { + console.log(val); + updateResourceChartImageMutation.mutate({ + input: { + id: params.chartID, + dataset: + getResourceChartDetailsRes?.data?.resourceChartImage?.dataset + ?.id, + name: val, + }, + }); + }} + loading={getResourceChartDetailsRes.isLoading} + status={ + updateResourceChartImageMutation.isLoading ? 'loading' : 'success' + } + setStatus={() => {}} + /> + + {getResourceChartDetailsRes?.isError ? ( +
+ Something went wrong +
+ ) : getResourceChartDetailsRes?.isLoading ? ( + + ) : ( +
+
+
+ {getResourceChartDetailsRes?.data?.resourceChartImage?.name} +
+ + {getResourceChartDetailsRes?.data?.resourceChartImage?.status === + 'DRAFT' ? ( + + ) : getResourceChartDetailsRes?.data?.resourceChartImage?.status === + 'PUBLISHED' ? ( + + ) : ( + + )} +
+
+ )} +
+ ); +}; + +export default ChartImagePreview; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/[chartID]/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/[chartID]/page.tsx new file mode 100644 index 00000000..ad1b775f --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/[chartID]/page.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { useParams, useSearchParams } from 'next/navigation'; + +import ChartGenVizPreview from './components/ChartGenVizPreview'; +import ChartImagePreview from './components/ChartImagePreview'; + +const ChartDetails = () => { + /* + Chart preview page to edit chart details and publish the chart + */ + + const params = useParams<{ + entityType: string; + entitySlug: string; + chartID: string; + }>(); + + const searchParams = useSearchParams(); + + const chartPreviewType = searchParams.get('type'); + + return chartPreviewType === 'TypeResourceChartImage' ? ( + + ) : ( + + ); +}; + +export default ChartDetails; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartEditor.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartEditor.tsx new file mode 100644 index 00000000..944d4be4 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartEditor.tsx @@ -0,0 +1,659 @@ +import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import { useParams, useRouter } from 'next/navigation'; +import { graphql } from '@/gql'; +import { + ResourceChartImageInput, + ResourceChartInput, +} from '@/gql/generated/graphql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { + Button, + Combobox, + Dialog, + DropZone, + Form, + Icon, + Label, + Select, + Spinner, + Tag, + Text, + toast, + Tooltip, +} from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { cn } from '@/lib/utils'; +import { Icons } from '@/components/icons'; + +const getAllDatasetsListwithResourcesDoc: any = graphql(` + query getAllDatasets { + datasets { + id + title + slug + resources { + id + name + } + } + } +`); + +const createResourceChartImageDoc: any = graphql(` + mutation createResourceChartImage($input: ResourceChartImageInput!) { + createResourceChartImage(input: $input) { + __typename + ... on TypeResourceChartImage { + name + id + } + } + } +`); + +const createResourceChartVizDoc: any = graphql(` + mutation createResourceChart($chartInput: ResourceChartInput!) { + createResourceChart(chartInput: $chartInput) { + __typename + ... on TypeResourceChart { + name + id + } + } + } +`); + +const ChartsEditor = ({ setEditorView }: { setEditorView: any }) => { + /* + Chart creation View in Listing Page to create either the image or the visualization + */ + + const params = useParams<{ entityType: string; entitySlug: string }>(); + + const allDatasetsRes: { + data: any; + isLoading: boolean; + refetch: any; + error: any; + isError: boolean; + } = useQuery([`allDatasetsListwithResourcesForCharts`], () => + GraphQL( + getAllDatasetsListwithResourcesDoc, + { + [params.entityType]: params.entitySlug, + }, + [] + ) + ); + + return ( +
+
+ Charts Editor + +
+
+ + Visual displays of information communicate complex data relationships + and data-driven insights in a way that is easy to understand. You can + create a Chart using our in-built chart generator, or Upload an Image. + +
+ + {allDatasetsRes.isLoading ? ( +
+ +
+ ) : ( +
+ + +
+ )} +
+ ); +}; + +export default ChartsEditor; + +const ChartImageUpload = ({ + allDatasetsRes, + params, +}: { + allDatasetsRes: any; + params: any; +}) => { + const [files, setFiles] = useState(undefined); + + const [selectedDataset, setSelectedDataset] = useState(null); + + const router = useRouter(); + + const createResourceChartImageMutation: { + mutate: any; + isLoading: boolean; + error: any; + } = useMutation( + [`createResourceChartImage`], + (input: ResourceChartImageInput) => + GraphQL( + createResourceChartImageDoc, + { + [params.entityType]: params.entitySlug, + }, + { input: input } + ), + { + onSuccess: (resp: any) => { + toast(`Created chart image successfully`); + // Navigate to chart image preview page + router.push( + `/dashboard/${params.entityType}/${params.entitySlug}/charts/${resp?.createResourceChartImage?.id}?type=TypeResourceChartImage` + ); + }, + onError: (err: any) => { + toast('Error: ' + err.message.split(':')[0]); + }, + } + ); + + const handleAddImage = () => { + if (selectedDataset && files) { + createResourceChartImageMutation.mutate({ + dataset: selectedDataset, + image: files, + }); + } + }; + + return ( +
+
+ + + IMAGE + +
+ + +
+ { + return { + label: item.title, + value: item.id, + }; + })} + displaySelected + // selectedValue={selectedDataset} + onChange={(e) => { + setSelectedDataset(e); + }} + required + /> + + { + setFiles(val[0]); + }} + outline + allowMultiple={false} + className="bg-greyExtralight" + errorOverlayText={files ? undefined : 'Please select a file'} + required + > + {files ? ( +
+ + {files.name} + +
+ ) : ( + + + Drag and drop + +
+ Select File +
+ + *only one image can be added. + + + Recommended resolution of 16:9 - (1280x720), (1920x1080) + + + Maximum file size: 100MB + +
+ + Supported File Types: + +
+ {['PNG', 'JPG', 'SVG', 'TIFF'].map((item, index) => ( + + {item} + + ))} +
+
+
+ } + actionTitle={''} + /> + )} + + +
+ +
+
+ +
+ ); +}; + +const ChartTypeDialog = () => { + const [open, setOpen] = useState(false); + const [selectedChartCategory, setSelectedChartCategory] = useState(''); + const [selectedChart, setSelectedChart] = useState(''); + + const chartTypes = [ + { + label: 'BAR HORIZONTAL', + value: 'BAR_HORIZONTAL', + image: '', + category: 'BAR', + categoryIcon: 'chartBar', + }, + { + label: 'BAR VERTICAL', + value: 'BAR_VERTICAL', + image: '', + category: 'BAR', + categoryIcon: 'chartBar', + }, + { + label: 'LINE', + value: 'LINE', + image: '', + category: 'LINE', + categoryIcon: 'chartLine', + }, + { + label: 'TREEMAP', + value: 'TREEMAP', + image: '', + category: 'TREEMAP', + categoryIcon: 'chartTreeMap', + }, + { + label: 'BIG NUMBER', + value: 'BIG_NUMBER', + image: '', + category: 'BIG_NUMBER', + categoryIcon: 'chartBigNumber', + }, + { + label: 'MAP', + value: 'MAP', + image: '', + category: 'MAP', + categoryIcon: 'chartMap', + }, + { + label: 'MAP POLYGON', + value: 'MAP_POLYGON', + image: '', + category: 'MAP_POLYGON', + categoryIcon: 'chartMapPolygon', + }, + ]; + + const categoriesArray = Array.from( + new Map( + chartTypes.map((item) => [ + item.category, + { value: item.category, icon: item.categoryIcon }, + ]) + ).values() + ); + + return ( +
+ + +
+ {categoriesArray.map((category, index) => ( +
{ + setSelectedChartCategory(category.value); + }} + > + + + {category.value} + +
+ ))} +
+
+ +
+
+
+ +
+
+
+ + Select Chart Type + +
+
+ {chartTypes + .filter((item) => item.category === selectedChartCategory) + .map((item) => ( +
{ + setSelectedChart(item.value); + }} + > + {item.label} + {item.label} +
+ ))} +
+
+
+ +
+ +
+
+
+
+
+ ); +}; + +const ChartCreateViz = ({ + allDatasetsRes, + params, +}: { + allDatasetsRes: any; + params: any; +}) => { + const [chartDataset, setChartDataset] = useState(''); + const [chartResource, setChartResource] = useState(''); + const [selectedChartType, setSelectedChartType] = useState(''); + + const router = useRouter(); + + const createResourceChartVizMutation: { + mutate: any; + isLoading: boolean; + error: any; + } = useMutation( + [`createResourceChart`], + (chartInput: ResourceChartInput) => + GraphQL( + createResourceChartVizDoc, + { + [params.entityType]: params.entitySlug, + }, + { chartInput: chartInput } + ), + { + onSuccess: (resp: any) => { + toast(`Created chart successfully. Redirecting . . .`); + // Navigate to chart preview page + router.push( + `/dashboard/${params.entityType}/${params.entitySlug}/charts/${resp?.createResourceChart?.id}?type=TypeResourceChart` + ); + }, + onError: (err: any) => { + console.error(err); + + toast('Error: ' + err.message.split(':')[0]); + }, + } + ); + + const chartTypes = [ + { + label: 'BAR', + value: 'BAR', + icon: 'chartBar', + }, + { + label: 'LINE', + value: 'LINE', + icon: 'chartLine', + }, + { + label: 'TREEMAP', + value: 'TREEMAP', + icon: 'chartTreeMap', + }, + { + label: 'BIG NUMBER', + value: 'BIG_NUMBER', + icon: 'chartBigNumber', + }, + { + label: 'MAP', + value: 'MAP', + icon: 'chartMap', + }, + { + label: 'MAP POLYGON', + value: 'MAP_POLYGON', + icon: 'chartMapPolygon', + }, + ]; + + const handleChartCreateViz = () => { + if (chartResource !== '' && selectedChartType !== '') { + createResourceChartVizMutation.mutate({ + resource: chartResource, + type: selectedChartType, + }); + } else { + toast('Please select a resource and chart type'); + } + }; + + return ( +
+
+ + + CHART + +
+
+
+ item.id === chartDataset) + ?.resources?.map((item: any) => { + return { + label: item.name, + value: item.id, + }; + })} + onChange={(e) => { + setChartResource(e); + }} + value={chartResource || ''} + /> +
+ + {/* */} +
+ {chartTypes.map((chartType, index) => ( + + ))} +
+
+ +
+ +
+
+
+
+ ); +}; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartForm.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartForm.tsx new file mode 100644 index 00000000..817f034c --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartForm.tsx @@ -0,0 +1,462 @@ +import React, { useEffect } from 'react'; +import { ChartTypes } from '@/gql/generated/graphql'; +import { Button, Checkbox, Select, Text, TextField } from 'opub-ui'; + +import { ChartData, ResourceData, ResourceSchema } from '../types'; + +interface ChartFormProps { + chartData: ChartData; + resourceData: ResourceData; + resourceSchema: ResourceSchema[]; + handleChange: (field: string, value: any) => void; + handleResourceChange: (value: string) => void; + handleSave: (data: ChartData) => void; +} + +const ChartForm: React.FC = ({ + chartData, + resourceData, + resourceSchema, + handleChange, + handleResourceChange, + handleSave, +}) => { + const isAssamChart = + chartData.type === ChartTypes.AssamDistrict || + chartData.type === ChartTypes.AssamRc; + const isGroupedChart = + chartData.type === ChartTypes.GroupedBarVertical || + chartData.type === ChartTypes.GroupedBarHorizontal || + chartData.type === ChartTypes.Multiline; + + const isBarOrLineChart = + chartData.type === ChartTypes.BarVertical || + chartData.type === ChartTypes.BarHorizontal || + chartData.type === ChartTypes.Line; + + useEffect(() => { + if ( + !chartData.options.yAxisColumn || + chartData.options.yAxisColumn.length === 0 + ) { + handleChange('options', { + ...chartData.options, + yAxisColumn: [{ fieldName: '', label: '', color: '#000000' }], + }); + } + }, [chartData.options.yAxisColumn]); + + console.log(chartData); + + useEffect(() => { + if (!chartData.filters || chartData.filters.length === 0) { + handleChange('filters', { + ...chartData, + filters: [{ column: '', operator: '==', value: '' }], + }); + } + }, [chartData.filters]); + + const handleYAxisColumnChange = ( + index: number, + field: string, + value: any + ) => { + const newYAxisColumns = [...chartData.options.yAxisColumn]; + newYAxisColumns[index] = { + ...newYAxisColumns[index], + [field]: value, + }; + handleChange('options', { + ...chartData.options, + yAxisColumn: newYAxisColumns, + }); + }; + + const addYAxisColumn = () => { + const newYAxisColumns = [ + ...(chartData.options.yAxisColumn ?? []), + { fieldName: '', label: '', color: '#000000' }, + ]; + handleChange('options', { + ...chartData.options, + yAxisColumn: newYAxisColumns, + }); + }; + + const removeYAxisColumn = (index: number) => { + const newYAxisColumns = chartData.options.yAxisColumn.filter( + (_, i) => i !== index + ); + handleChange('options', { + ...chartData.options, + yAxisColumn: newYAxisColumns, + }); + handleSave(chartData); + }; + + const handlefilterColumnChange = ( + index: number, + field: string, + value: any + ) => { + const currentFilters = Array.isArray(chartData.filters) + ? chartData.filters + : []; + const newFiltersColumns = [...currentFilters]; + newFiltersColumns[index] = { + ...newFiltersColumns[index], + [field]: value, + }; + handleChange('filters', newFiltersColumns); // Changed this line + }; + const addFilterColumn = () => { + const currentFilters = Array.isArray(chartData.filters) + ? chartData.filters + : []; + const newFiltersColumns = [ + ...currentFilters, + { column: '', operator: '==', value: '' }, + ]; + handleChange('filters', newFiltersColumns); + }; + + const removeFilterColumn = (index: number) => { + const currentFilters = Array.isArray(chartData.filters) + ? chartData.filters + : []; + const newFiltersColumns = currentFilters.filter((_, i) => i !== index); + handleChange('filters', newFiltersColumns); + handleSave(chartData); + }; + const updateChartData = (field: string, value: any) => { + if (field === 'type') { + const newData = { + ...chartData, + type: value, + options: { + ...chartData.options, + yAxisColumn: + chartData.options.yAxisColumn.length > 0 + ? chartData.options.yAxisColumn + : [{ fieldName: '', label: '', color: '#000000' }], + }, + }; + handleChange(field, value); + handleSave(newData); // Pass the new data directly + } else { + const newData = { + ...chartData, + [field]: value, + }; + handleChange(field, value); + handleSave(newData); + } + }; + + return ( +
+ handleChange('name', e)} + label="Name" + value={chartData.name} + name="name" + onBlur={() => handleSave(chartData)} + required + /> + handleChange('description', e)} + label="Description" + value={chartData.description} + name="description" + onBlur={() => handleSave(chartData)} + required + /> + ({ + label: resource.name, + value: resource.id, + }))} + label="Resource" + value={chartData.resource} + onBlur={() => handleSave(chartData)} + onChange={handleResourceChange} + placeholder="Select" + /> +
+ ({ + label: field.fieldName, + value: field.id, + }))} + label="X-axis Column" + value={chartData.options.xAxisColumn} + onBlur={() => handleSave(chartData)} + onChange={(e) => + handleChange('options', { + ...chartData.options, + xAxisColumn: e, + }) + } + placeholder="Select" + /> +
+ + handleChange('options', { + ...chartData.options, + xAxisLabel: e, + }) + } + label="X-axis Label" + value={chartData.options.xAxisLabel} + name="xAxisLabel" + onBlur={() => handleSave(chartData)} + required + /> + + handleChange('options', { + ...chartData.options, + yAxisLabel: e, + }) + } + label="Y-axis Label" + value={chartData.options.yAxisLabel} + name="yAxisLabel" + onBlur={() => handleSave(chartData)} + required + /> +
+ + {(isBarOrLineChart || isGroupedChart) && ( +
+
+ {chartData?.options?.yAxisColumn?.map((column, index) => ( +
+ { + handleYAxisColumnChange( + index, + 'color', + e.target.value + ); + }} + onBlur={() => handleSave(chartData)} + /> +
+ {isGroupedChart && index > 0 && ( + + )} +
+ ))} +
+ {isGroupedChart && ( + + )} +
+ )} + + )} + + {isAssamChart && ( + <> + ({ + label: field.fieldName, + value: field.id, + }))} + label="Value Column" + value={chartData.options.valueColumn} + onBlur={() => handleSave(chartData)} + onChange={(e) => + handleChange('options', { + ...chartData.options, + valueColumn: e, + }) + } + placeholder="Select" + /> + + )} + +
+
+ {Array.isArray(chartData?.filters) && + chartData?.filters?.map((column, index) => ( +
+ ' }, + { label: 'In', value: 'in' }, + { label: 'Not In', value: 'not in' }, + { label: 'Less than or Equal to', value: '<=' }, + { label: 'Greater than or Equal to', value: '>=' }, + ]} + label="Operator" + value={column.operator} + defaultValue="Equal to" + onChange={(e) => + handlefilterColumnChange(index, 'operator', e) + } + onBlur={() => handleSave(chartData)} + /> + + { + handlefilterColumnChange(index, 'value', e); + }} + onBlur={() => handleSave(chartData)} + /> + { + + } +
+ ))} +
+ { + + } +
+
+ ); +}; + +export default ChartForm; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartHeader.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartHeader.tsx new file mode 100644 index 00000000..a5b29150 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartHeader.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { Button, Icon, Sheet, Text } from 'opub-ui'; + +import { Icons } from '@/components/icons'; +import { ResourceData } from '../types'; + +interface ChartHeaderProps { + setType: (type: string) => void; + setChartId: (id: string) => void; + isSheetOpen: boolean; + setIsSheetOpen: (open: boolean) => void; + resourceChart: { + mutate: (data: { resource: string }) => void; + isLoading: boolean; + }; + resourceData: ResourceData; + chartsList: { + chartsDetails: Array<{ + id: string; + name: string; + }>; + } | null; + chartId: string; +} + +const ChartHeader: React.FC = ({ + setType, + setChartId, + isSheetOpen, + setIsSheetOpen, + resourceChart, + resourceData, + chartsList, + chartId, +}) => { + return ( +
+ + + + + + +
+
+ Select Charts +
+ + +
+
+ {chartsList?.chartsDetails.map((item, index) => ( +
+ +
+ ))} +
+
+
+
+ ); +}; + +export default ChartHeader; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsImage.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsImage.tsx new file mode 100644 index 00000000..6ca33f25 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsImage.tsx @@ -0,0 +1,362 @@ +import { UUID } from 'crypto'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import { graphql } from '@/gql'; +import { ResourceChartImageInputPartial } from '@/gql/generated/graphql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { + Button, + Divider, + DropZone, + Icon, + Sheet, + Spinner, + Text, + TextField, + toast, +} from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { Icons } from '@/components/icons'; +import { Loading } from '@/components/loading'; + +interface ImageProps { + setType: any; + setImageId: any; + imageId: any; +} + +const getResourceChartImageDetails: any = graphql(` + query resourceChartImage($filters: ResourceChartImageFilter) { + resourceChartImages(filters: $filters) { + id + name + description + image { + name + path + } + } + } +`); + +const getDatasetResourceChartImageDetails: any = graphql(` + query resourceChartImages($datasetId: UUID!) { + datasetResourceCharts(datasetId: $datasetId) { + id + name + description + image { + name + path + } + } + } +`); + +const AddResourceChartimage: any = graphql(` + mutation GenerateResourceChartimage($dataset: UUID!) { + addResourceChartImage(dataset: $dataset) { + __typename + ... on TypeResourceChartImage { + id + name + } + } + } +`); + +const UpdateChartImageMutation: any = graphql(` + mutation updateChartImage($input: ResourceChartImageInputPartial!) { + updateResourceChartImage(input: $input) { + __typename + ... on TypeResourceChartImage {{} + id + name + description + image { + name + path + } +} + } + } +`); + +const ChartsImage: React.FC = ({ + setType, + setImageId, + imageId, +}) => { + const params = useParams<{ + entityType: string; + entitySlug: string; + id: string; + }>(); + + const { data: chartImageDetails, refetch }: { data: any; refetch: any } = + useQuery( + [`chartsdata_${params.id}`, imageId], + () => + GraphQL( + getResourceChartImageDetails, + { + [params.entityType]: params.entitySlug, + }, + { + filters: { + id: imageId, + }, + } + ), + {} + ); + + const { + data: chartImagesList, + refetch: listrefetch, + }: { data: any; refetch: any } = useQuery( + [`chartslist_${params.id}`, imageId], + () => + GraphQL( + getDatasetResourceChartImageDetails, + { + [params.entityType]: params.entitySlug, + }, + { + datasetId: params.id, + } + ), + {} + ); + + const resourceChartImageMutation: { + mutate: any; + isLoading: any; + } = useMutation( + (data: { dataset: UUID }) => + GraphQL( + AddResourceChartimage, + { + [params.entityType]: params.entitySlug, + }, + data + ), + { + onSuccess: (res: any) => { + toast('Resource ChartImage Created Successfully'); + setType('img'); + setImageId(res.addResourceChartImage.id); + setIsSheetOpen(false); + }, + onError: (err: any) => { + toast(`Received ${err} while deleting chart `); + }, + } + ); + + const initialFormData = { + id: '', + name: '', + description: '', + image: null, + }; + + const [formData, setFormData] = useState(initialFormData); + const [previousFormData, setPreviousFormData] = useState(initialFormData); + + const [isSheetOpen, setIsSheetOpen] = useState(false); + + useEffect(() => { + if (chartImageDetails?.resourceChartImages[0]) { + const updatedData = { + name: chartImageDetails?.resourceChartImages[0].name || '', + description: + chartImageDetails?.resourceChartImages[0].description || '', + image: chartImageDetails?.resourceChartImages[0].image || null, + id: chartImageDetails?.resourceChartImages[0].id, + }; + setFormData(updatedData); + setPreviousFormData(updatedData); + } + }, [chartImageDetails]); + + const { mutate, isLoading: editMutationLoading } = useMutation( + (data: { data: ResourceChartImageInputPartial }) => + GraphQL(UpdateChartImageMutation, {}, data), + { + onSuccess: () => { + toast('ChartImage updated successfully'); + // Optionally, reset form or perform other actions + refetch(); + listrefetch(); + }, + onError: (error: any) => { + toast(`Error: ${error.message}`); + }, + } + ); + + const handleChange = useCallback((field: string, value: any) => { + setFormData((prevData) => ({ + ...prevData, + [field]: value, + })); + }, []); + + const onDrop = React.useCallback( + (_dropFiles: File[], acceptedFiles: File[]) => { + // mutate({ + // data: { + // id: imageId, + // image: acceptedFiles[0], + // }, + // }); + { + refetch(); + listrefetch(); + } + }, + [] + ); + + const handleSave = (updatedData: any) => { + if (JSON.stringify(formData) !== JSON.stringify(previousFormData)) { + setPreviousFormData(updatedData); + + // mutate({ + // data: { + // id: imageId, + // name: updatedData.name, + // description: updatedData.description, + // }, + // }); + } + }; + + return ( + <> + {!imageId ? ( + + ) : ( +
+
+ + + + + + +
+
+ Select Charts +
+ + +
+
+ {chartImagesList?.datasetResourceCharts.map( + (item: any, index: any) => ( +
+ +
+ ) + )} +
+
+
+
+ +
+ Auto Save + {editMutationLoading ? ( + + ) : ( + + )} +
+
+
+ handleChange('name', e)} + onBlur={() => handleSave(formData)} + /> + handleChange('description', e)} + onBlur={() => handleSave(formData)} + /> + + + +
+
+
+ )} + + ); +}; + +export default ChartsImage; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsList.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsList.tsx new file mode 100644 index 00000000..51be50b8 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsList.tsx @@ -0,0 +1,387 @@ +import { UUID } from 'crypto'; +import React, { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { graphql } from '@/gql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { + Button, + DataTable, + IconButton, + SearchInput, + Spinner, + Text, + toast, +} from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { toTitleCase } from '@/lib/utils'; +import { Icons } from '@/components/icons'; +import ChartEditor from './ChartEditor'; + +interface ChartsListProps { + setType: any; + setChartId: any; + setImageId: any; +} + +const getAllCharts: any = graphql(` + query ChartList { + getChartData { + __typename + ... on TypeResourceChart { + name + id + chartType + dataset { + title + slug + id + } + resource { + name + id + } + } + ... on TypeResourceChartImage { + name + id + dataset { + title + slug + id + } + status + } + } + } +`); + +const deleteResourceChart: any = graphql(` + mutation deleteResourceChart($chartId: UUID!) { + deleteResourceChart(chartId: $chartId) + } +`); + +const deleteResourceChartImage: any = graphql(` + mutation deleteResourceChartImage($resourceChartImageId: UUID!) { + deleteResourceChartImage(resourceChartImageId: $resourceChartImageId) + } +`); + +// const AddResourceChartImage: any = graphql(` +// mutation GenerateResourceChartImage($dataset: UUID!) { +// addResourceChartImage(dataset: $dataset) { +// __typename +// ... on TypeResourceChartImage { +// id +// name +// } +// } +// } +// `); + +// const AddResourceChart: any = graphql(` +// mutation GenerateResourceChart($resource: UUID!) { +// addResourceChart(resource: $resource) { +// __typename +// ... on TypeResourceChart { +// id +// name +// } +// } +// } +// `); + +// const datasetResourceList: any = graphql(` +// query all_resources($datasetId: UUID!) { +// datasetResources(datasetId: $datasetId) { +// id +// type +// name +// } +// } +// `); + +const ChartsList: React.FC = ({ + setType, + setChartId, + setImageId, +}) => { + const params = useParams<{ + entityType: string; + entitySlug: string; + id: string; + }>(); + + const router = useRouter(); + + const [editorView, setEditorView] = useState(false); + + const chartListRes: { + data: any; + isLoading: boolean; + refetch: any; + error: any; + isError: boolean; + } = useQuery([`chartList`], () => + GraphQL( + getAllCharts, + { + [params.entityType]: params.entitySlug, + }, + [] + ) + ); + + const [filteredRows, setFilteredRows] = useState([]); + + useEffect(() => { + chartListRes.refetch(); + if (chartListRes.data?.getChartData) { + setFilteredRows(chartListRes.data.getChartData); + } + }, [chartListRes.data]); + + const deleteResourceChartmutation: { mutate: any; isLoading: any } = + useMutation( + (data: { chartId: UUID }) => + GraphQL( + deleteResourceChart, + { + [params.entityType]: params.entitySlug, + }, + data + ), + { + onSuccess: () => { + toast('Chart Deleted Successfully'); + chartListRes.refetch(); + }, + onError: (err: any) => { + toast(`Received ${err} while deleting chart `); + }, + } + ); + + const deleteResourceChartImagemutation: { mutate: any; isLoading: any } = + useMutation( + (data: { resourceChartImageId: string }) => + GraphQL( + deleteResourceChartImage, + { + [params.entityType]: params.entitySlug, + }, + data + ), + { + onSuccess: () => { + toast('ChartImage Deleted Successfully'); + chartListRes.refetch(); + }, + onError: (err: any) => { + toast(`Received ${err} while deleting chart `); + }, + } + ); + + // const resourceChartImageMutation: { + // mutate: any; + // isLoading: any; + // } = useMutation( + // (data: { dataset: UUID }) => + // GraphQL( + // AddResourceChartImage, + // { + // [params.entityType]: params.entitySlug, + // }, + // data + // ), + // { + // onSuccess: (res: any) => { + // toast('Resource ChartImage Created Successfully'); + // chartListRes.refetch(); + // setType('img'); + // setImageId(res.addResourceChartImage.id); + + // // setImageId(res.id); + // }, + // onError: (err: any) => { + // toast(`Received ${err} while deleting chart `); + // }, + // } + // ); + + // AddResourceImage + + // const resourceList: { data: any } = useQuery([`charts_${params.id}`], () => + // GraphQL( + // datasetResourceList, + // { + // [params.entityType]: params.entitySlug, + // }, + // { datasetId: params.id } + // ) + // ); + + // const resourceChart: { + // mutate: any; + // isLoading: any; + // } = useMutation( + // (data: { resource: UUID }) => + // GraphQL( + // AddResourceChart, + // { + // [params.entityType]: params.entitySlug, + // }, + // data + // ), + // { + // onSuccess: (res: any) => { + // toast('Resource Chart Created Successfully'); + // chartListRes.refetch(); + // setType('visualize'); + // setChartId(res.addResourceChart.id); + + // // setImageId(res.id); + // }, + // onError: (err: any) => { + // toast(`Received ${err} while deleting chart `); + // }, + // } + // ); + + const handleChart = (row: any) => { + if (row.original.typename === 'TypeResourceChart') { + router.push( + `/dashboard/${params.entityType}/${params.entitySlug}/charts/${row.original.id}?type=TypeResourceChart` + ); + } else { + router.push( + `/dashboard/${params.entityType}/${params.entitySlug}/charts/${row.original.id}?type=TypeResourceChartImage` + ); + } + }; + + const generateColumnData = () => { + return [ + { + accessorKey: 'name', + header: 'Name of Chart', + cell: ({ row }: any) => ( +
handleChart(row)} + > + {row.original.name} +
+ ), + }, + + { + accessorKey: 'type', + header: 'Chart type', + }, + { + accessorKey: 'dataset', + header: 'Dataset', + }, + { + accessorKey: 'resource', + header: 'Resource', + }, + { + accessorKey: 'status', + header: 'Status', + }, + { + header: 'DELETE', + cell: ({ row }: any) => ( +
+ { + row.original.typename === 'TypeResourceChart' + ? deleteResourceChartmutation.mutate({ + chartId: row.original.id, + }) + : deleteResourceChartImagemutation.mutate({ + resourceChartImageId: row.original.id, + }); + }} + > + Delete + +
+ ), + }, + ]; + }; + + const generateTableData = (data: any[]) => { + return data?.map((item: any) => ({ + name: item.name, + type: item.chartType + ? toTitleCase(item.chartType.split('_').join(' ').toLowerCase()) + : 'Image', + id: item.id, + resource: item.resource?.name || '', + dataset: item.dataset?.title || item.dataset?.id || '', + typename: item.__typename, + status: item.status || 'NA', + })); + }; + + const handleSearchChange = (e: string) => { + const searchTerm = e.toLowerCase(); + const filtered = chartListRes.data?.getChartData.filter((row: any) => + row.name.toLowerCase().includes(searchTerm) + ); + setFilteredRows(filtered || []); + }; + + return ( + <> + {editorView ? ( + + ) : chartListRes.isLoading || deleteResourceChartmutation.isLoading ? ( +
+ +
+ ) : chartListRes.isError ? ( + <>Error + ) : ( + <> +
+ Showing Charts + handleSearchChange(e)} + /> +
+ +
+
+ {filteredRows.length > 0 ? ( + + ) : ( + <>No records found + )} + + )} + + ); +}; + +export default ChartsList; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsVisualize.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsVisualize.tsx new file mode 100644 index 00000000..75c06590 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsVisualize.tsx @@ -0,0 +1,465 @@ +import { UUID } from 'crypto'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useParams } from 'next/navigation'; +import { renderGeoJSON } from '@/geo_json/render_geojson'; +import { ChartTypes } from '@/gql/generated/graphql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import ReactECharts from 'echarts-for-react'; +import * as echarts from 'echarts/core'; +import { Button, Divider, Icon, Sheet, Spinner, Text, toast } from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { Icons } from '@/components/icons'; +import { + chartDetailsQuery, + createChart, + CreateResourceChart, + datasetResource, + getResourceChartDetails, +} from '../queries'; +import ChartForm from './ChartForm'; +import ChartHeader from './ChartHeader'; + +interface YAxisColumnItem { + fieldName: string; + label: string; + color: string; +} +interface ChartFilters { + column: string; + operator: string; + value: string; +} + +interface ChartOptions { + aggregateType: string; + regionColumn?: string; + showLegend: boolean; + timeColumn?: string; + valueColumn?: string; + xAxisColumn: string; + xAxisLabel: string; + yAxisColumn: YAxisColumnItem[]; + yAxisLabel: string; +} + +interface ChartData { + chartId: string; + description: string; + filters: any[]; + name: string; + options: ChartOptions; + resource: string; + type: ChartTypes; + chart: any; +} + +interface ResourceChartInput { + chartId: string; + description: string; + filters: ChartFilters[]; + name: string; + options: ChartOptions; + resource: string; + type: ChartTypes; +} + +interface VisualizationProps { + setType: any; + setChartId: any; + chartId: any; +} + +const ChartsVisualize: React.FC = ({ + setType, + chartId, + setChartId, +}) => { + const params = useParams<{ + entityType: string; + entitySlug: string; + id: string; + }>(); + + const { data: resourceData }: { data: any } = useQuery( + [`res_charts_${params.id}`], + () => + GraphQL( + datasetResource, + { + [params.entityType]: params.entitySlug, + }, + { datasetId: params.id } + ) + ); + + const { data: chartDetails, refetch }: { data: any; refetch: any } = useQuery( + [`chartdata_${params.id}`], + () => + GraphQL( + getResourceChartDetails, + { + [params.entityType]: params.entitySlug, + }, + { + chartDetailsId: chartId, + } + ), + {} + ); + + const { + data: chartsList, + isLoading, + refetch: chartsListRefetch, + }: { data: any; isLoading: boolean; refetch: any } = useQuery( + [`chartsList_${params.id}`], + () => + GraphQL( + chartDetailsQuery, + { + [params.entityType]: params.entitySlug, + }, + { + datasetId: params.id, + } + ) + ); + + const resourceChart: { + mutate: any; + isLoading: any; + } = useMutation( + (data: { resource: UUID }) => + GraphQL( + CreateResourceChart, + { + [params.entityType]: params.entitySlug, + }, + data + ), + { + onSuccess: (res: any) => { + toast('Resource Chart Created Successfully'); + refetch(); + setIsSheetOpen(false); + setType('visualize'); + setChartId(res.addResourceChart.id); + chartsListRefetch(); + }, + onError: (err: any) => { + toast(`Received ${err} while deleting chart `, { + action: { + label: 'undo', + onClick: () => {}, + }, + }); + }, + } + ); + + const [isSheetOpen, setIsSheetOpen] = useState(false); + const [chartData, setChartData] = useState({ + chartId: '', + description: '', + filters: [ + { + column: '', + operator: '==', + value: '', + }, + ], + name: '', + options: { + aggregateType: 'SUM', + showLegend: true, + xAxisColumn: '', + xAxisLabel: '', + yAxisColumn: [{ fieldName: '', label: '', color: '#000000' }], + yAxisLabel: '', + regionColumn: '', + valueColumn: '', + timeColumn: '', + }, + resource: '', + type: ChartTypes.BarVertical, + chart: {}, + }); + + const [previousChartData, setPreviousChartData] = useState( + null + ); + + const [resourceSchema, setResourceSchema] = useState([]); + + useEffect(() => { + if (chartId && chartDetails?.resourceChart) { + const resource = resourceData?.datasetResources?.find( + (r: any) => r.id === chartDetails.resourceChart.resource?.id + ); + + if (resource) { + setResourceSchema(resource.schema || []); + } + } + }, [chartId, chartDetails, resourceData]); + + useEffect(() => { + if (chartId && chartDetails?.resourceChart) { + refetch(); + updateChartData(chartDetails.resourceChart); + } + }, [chartId, chartDetails]); + + const updateChartData = (resourceChart: any) => { + if ( + resourceChart.chartType === 'ASSAM_DISTRICT' || + resourceChart.chartType === 'ASSAM_RC' + ) { + echarts.registerMap( + resourceChart.chartType.toLowerCase(), + renderGeoJSON(resourceChart.chartType.toLowerCase()) + ); + } + + const updatedData: ChartData = { + chartId: resourceChart.id, + description: resourceChart.description || '', + filters: + resourceChart.chartFilters?.length > 0 + ? resourceChart.chartFilters.map((filter: any) => ({ + column: filter.column?.id, + operator: filter.operator, + value: filter.value, + })) + : [{ column: '', operator: '==', value: '' }], + name: resourceChart.name || '', + options: { + aggregateType: resourceChart?.chartOptions?.aggregateType, + regionColumn: resourceChart?.chartOptions?.regionColumn?.id, + showLegend: resourceChart?.chartOptions?.showLegend ?? true, + timeColumn: resourceChart?.chartOptions?.timeColumn, + valueColumn: resourceChart?.chartOptions?.valueColumn?.id, + xAxisColumn: resourceChart?.chartOptions?.xAxisColumn?.id, + xAxisLabel: resourceChart?.chartOptions?.xAxisLabel, + yAxisColumn: resourceChart?.chartOptions?.yAxisColumn?.map( + (col: any) => ({ + fieldName: col.field.id, + label: col.label, + color: col.color, + }) + ), + yAxisLabel: resourceChart?.chartOptions?.yAxisLabel, + }, + resource: resourceChart.resource?.id, + type: resourceChart.chartType as ChartTypes, + chart: resourceChart.chart, + }; + setChartData(updatedData); + setPreviousChartData(updatedData); + }; + + const getDefaultOptions = (chartType: ChartTypes) => { + const baseOptions = { + aggregateType: 'SUM', + showLegend: true, + xAxisColumn: '', + xAxisLabel: '', + yAxisLabel: '', + }; + + switch (chartType) { + case ChartTypes.AssamDistrict: + case ChartTypes.AssamRc: + return { + ...baseOptions, + regionColumn: '', + valueColumn: '', + timeColumn: '', + yAxisColumn: [], + }; + case ChartTypes.BarVertical: + case ChartTypes.BarHorizontal: + return { + ...baseOptions, + yAxisColumn: [{ fieldName: '', label: '', color: '#000000' }], + }; + default: + return { + ...baseOptions, + yAxisColumn: [{ fieldName: '', label: '', color: '#000000' }], + }; + } + }; + + const handleChange = useCallback((field: string, value: any) => { + setChartData((prevData) => { + if (field === 'type') { + const newType = value as ChartTypes; + return { + ...prevData, + type: newType, + options: getDefaultOptions(newType), + }; + } + if (field === 'options') { + return { + ...prevData, + options: { + ...prevData.options, + ...value, + }, + }; + } + return { + ...prevData, + [field]: value, + }; + }); + }, []); + + const { mutate, isLoading: editMutationLoading } = useMutation( + (chartInput: { chartInput: ResourceChartInput }) => + GraphQL( + createChart, + { + [params.entityType]: params.entitySlug, + }, + chartInput + ), + { + onSuccess: (res: any) => { + toast('Resource chart saved'); + const newChartId = res?.editResourceChart?.id; + updateChartData(res.editResourceChart); + setChartId(newChartId); + chartsListRefetch(); + refetch(); + }, + onError: (err: any) => { + toast(`Received ${err} during resource chart saving`, { + action: { + label: 'undo', + onClick: () => {}, + }, + }); + }, + } + ); + + const handleSave = useCallback( + (updatedData: ChartData) => { + if (JSON.stringify(previousChartData) !== JSON.stringify(updatedData)) { + // Filter out empty Y-axis columns + const validYAxisColumns = updatedData.options.yAxisColumn.filter( + (col) => col.fieldName && col.fieldName.trim() !== '' + ); + + const chartInput: ResourceChartInput = { + chartId: updatedData.chartId, + description: updatedData.description, + filters: updatedData.filters, + name: updatedData.name, + options: { + ...updatedData.options, + yAxisColumn: validYAxisColumns, + }, + resource: updatedData.resource, + type: updatedData.type, + }; + + // Store current type before mutation + const currentType = updatedData.type; + + mutate( + { chartInput }, + { + onSuccess: (data) => { + setChartData((prev) => ({ + ...prev, + chart: data.chart, + type: currentType, // Preserve the type from before mutation + options: { + ...prev.options, + yAxisColumn: validYAxisColumns, + }, + })); + }, + } + ); + + setPreviousChartData({ + ...updatedData, + type: currentType, // Ensure previousChartData also has correct type + filters: + updatedData.filters?.length > 0 + ? updatedData.filters + : [{ column: '', operator: '==', value: '' }], + }); + } + }, + [previousChartData, mutate] + ); + + const handleResourceChange = useCallback( + (value: string) => { + const resource = resourceData?.datasetResources.find( + (r: any) => r.id === value + ); + if (resource) { + handleChange('resource', resource.id); + } + }, + [resourceData, handleChange] + ); + + const chartRef = useRef(null); + + return ( + <> +
+ + +
+
+ Auto Save + {editMutationLoading ? ( + + ) : ( + + )} +
+ +
+ Preview + {chartData.chart?.options && + Object.keys(chartData.chart?.options).length > 0 && ( + + )} +
+
+
+ + ); +}; + +export default ChartsVisualize; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/page.tsx new file mode 100644 index 00000000..55e22a99 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/page.tsx @@ -0,0 +1,44 @@ +'use client'; + +import React from 'react'; +import { parseAsString, useQueryState } from 'next-usequerystate'; + +import ChartsImage from './components/ChartsImage'; +import ChartsList from './components/ChartsList'; +import ChartsVisualize from './components/ChartsVisualize'; + +const Charts = () => { + const [type, setType] = useQueryState( + 'type', + parseAsString.withDefault('list') + ); + + const [chartId, setChartId] = useQueryState('id', parseAsString); + const [imageId, setImageId] = useQueryState('id', parseAsString); + + return ( +
+ {type === 'list' ? ( + + ) : type === 'visualize' ? ( + + ) : ( + + )} +
+ ); +}; + +export default Charts; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/queries/index.ts b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/queries/index.ts new file mode 100644 index 00000000..a1fdfa97 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/queries/index.ts @@ -0,0 +1,145 @@ +import { graphql } from '@/gql'; + +export const datasetResource = graphql(` + query allresources($datasetId: UUID!) { + datasetResources(datasetId: $datasetId) { + id + type + name + description + schema { + fieldName + id + format + } + } + } +`); + +export const chartDetailsQuery = graphql(` + query chartsDetails($datasetId: UUID!) { + chartsDetails(datasetId: $datasetId) { + id + name + chartType + } + } +`); + +export const getResourceChartDetails = graphql(` + query resourceChart($chartDetailsId: UUID!) { + resourceChart(chartDetailsId: $chartDetailsId) { + chartType + description + id + name + resource { + id + } + chart { + options + } + chartFilters { + column { + id + fieldName + } + operator + value + } + chartOptions { + aggregateType + xAxisColumn { + id + fieldName + } + yAxisColumn { + field { + id + fieldName + } + label + color + } + showLegend + xAxisLabel + yAxisLabel + regionColumn { + id + fieldName + } + valueColumn { + id + fieldName + } + } + } + } +`); + +export const createChart = graphql(` + mutation editResourceChart($chartInput: ResourceChartInput!) { + editResourceChart(chartInput: $chartInput) { + __typename + ... on TypeResourceChart { + chartType + description + id + resource { + id + name + } + name + chartFilters { + column { + id + fieldName + } + operator + value + } + chartOptions { + aggregateType + xAxisColumn { + id + fieldName + } + yAxisColumn { + field { + id + fieldName + } + label + color + } + showLegend + xAxisLabel + yAxisLabel + regionColumn { + id + fieldName + } + valueColumn { + id + fieldName + } + } + chart { + options + } + } + } + } +`); + +export const CreateResourceChart: any = graphql(` + mutation GenerateResourceChart($resource: UUID!) { + addResourceChart(resource: $resource) { + __typename + ... on TypeResourceChart { + id + name + } + } + } +`); diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/types.ts b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/types.ts new file mode 100644 index 00000000..9f84dc8e --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/types.ts @@ -0,0 +1,62 @@ +import { ChartTypes } from "@/gql/generated/graphql"; + + + +export interface YAxisColumnItem { + fieldName: string; + label: string; + color: string; +} + +export interface ChartFilters{ + column: string; + operator: string; + value: string; +} +export interface ChartOptions { + aggregateType: string; + regionColumn?: string; + showLegend: boolean; + timeColumn?: string; + valueColumn?: string; + xAxisColumn: string; + xAxisLabel: string; + yAxisColumn: YAxisColumnItem[]; + yAxisLabel: string; +} + +export interface ChartData { + chartId: string; + description: string; + filters: any[]; + name: string; + options: ChartOptions; + resource: string; + type: ChartTypes; + chart: any; +} + +export interface ResourceChartInput { + chartId: string; + description: string; + filters: ChartFilters[]; + name: string; + options: ChartOptions; + resource: string; + type: ChartTypes; +} + +export interface ResourceSchema { + fieldName: string; + id: string; + format: string +} + +export interface Resource { + id: string; + name: string; +} + +export interface ResourceData { + datasetResources: Resource[]; +} diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/components/StepNavigation.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/components/StepNavigation.tsx new file mode 100644 index 00000000..18c1f9bc --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/components/StepNavigation.tsx @@ -0,0 +1,88 @@ +import { usePathname, useRouter } from 'next/navigation'; +import { Button, Icon, Text } from 'opub-ui'; + +import { Icons } from '@/components/icons'; + +interface StepNavigationProps { + steps: string[]; // Array of steps (e.g., ['metadata', 'details', 'publish']) +} + +const StepNavigation = ({ steps }: StepNavigationProps) => { + const pathname = usePathname(); // Get the current URL path + const router = useRouter(); + + // Find the current step's index based on the pathname (without query params) + const currentIndex = steps.findIndex((step) => + pathname.split('?')[0].endsWith(step) + ); + + if (currentIndex === -1) { + return null; // In case no valid step is found + } + + const goToPrevious = () => { + if (currentIndex > 0) { + const newPath = pathname.replace( + steps[currentIndex], + steps[currentIndex - 1] + ); + router.push(newPath); // Update the URL to the previous step + } + }; + + const goToNext = () => { + if (currentIndex < steps.length - 1) { + const newPath = pathname.replace( + steps[currentIndex], + steps[currentIndex + 1] + ); + router.push(newPath); // Update the URL to the next step + } + }; + + return ( +
+ + + Step {currentIndex + 1} of {steps.length} + + +
+ ); +}; + +export default StepNavigation; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/components/title-bar.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/components/title-bar.tsx new file mode 100644 index 00000000..a58ac924 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/components/title-bar.tsx @@ -0,0 +1,97 @@ +import Link from 'next/link'; +import { Button, Icon, Spinner, Text, TextField } from 'opub-ui'; +import { useEffect, useState } from 'react'; + +import { Icons } from '@/components/icons'; + +interface TitleBarProps { + label: string; + title: string; + goBackURL: string; + onSave: (data: any) => void; + loading: boolean; + status: 'loading' | 'success'; + setStatus: (s: 'loading' | 'success') => void;} + +const TitleBar = ({ + label, + title, + goBackURL, + onSave, + loading, + status, + setStatus, +}: TitleBarProps) => { + const [edit, setEdit] = useState(false); + const [titleData, setTitleData] = useState(title); + + useEffect(() => { + setStatus(loading ? 'loading' : 'success'); + }, [loading]); + + return ( +
+
+ {label}: + {edit ? ( + { + setTitleData(e); + }} + /> + ) : ( + {title} + )} +
+ {edit && ( + + )} + + {!edit && ( + + )} +
+
+
+
+ {' '} + + {status === 'loading' ? 'Saving...' : 'All Changes Saved'} + + {status === 'loading' ? ( + + ) : ( + + )} +
+ + {' '} + + Close Editor + + +
+
+ ); +}; + +export default TitleBar; diff --git a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/access/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/access/page.tsx similarity index 100% rename from app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/access/page.tsx rename to app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/access/page.tsx diff --git a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/AccessModelForm.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelForm.tsx similarity index 95% rename from app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/AccessModelForm.tsx rename to app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelForm.tsx index 20981122..74e3f296 100644 --- a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/AccessModelForm.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelForm.tsx @@ -73,11 +73,8 @@ const editaccessModel: any = graphql(` const getAccessModelDetails: any = graphql(` query accessModel($accessModelId: UUID!) { accessModel(accessModelId: $accessModelId) { - modelResources { - fields { - id - fieldName - } + resourceFields { + fields resource { id name @@ -102,10 +99,21 @@ const AccessModelForm: React.FC = ({ setList(false); }, []); - const params = useParams(); + const params = useParams<{ + entityType: string; + entitySlug: string; + id: string; + }>(); const { data, isLoading }: { data: any; isLoading: boolean } = useQuery( [`resourcesList_${params.id}`], - () => GraphQL(datasetResourcesQuery, { datasetId: params.id }) + () => + GraphQL( + datasetResourcesQuery, + { + [params.entityType]: params.entitySlug, + }, + { datasetId: params.id } + ) ); const { @@ -114,7 +122,14 @@ const AccessModelForm: React.FC = ({ refetch: accessModelListRefetch, }: { data: any; isLoading: boolean; refetch: any } = useQuery( [`accessModelList_${params.id}`], - () => GraphQL(accessModelListQuery, { datasetId: params.id }) + () => + GraphQL( + accessModelListQuery, + { + [params.entityType]: params.entitySlug, + }, + { datasetId: params.id } + ) ); const { data: accessModelDetails, @@ -122,7 +137,14 @@ const AccessModelForm: React.FC = ({ isLoading: accessModelDetailsLoading, }: { data: any; isLoading: boolean; refetch: any } = useQuery( [`accessModelDetails${params.id}`], - () => GraphQL(getAccessModelDetails, { accessModelId: accessModelId }) + () => + GraphQL( + getAccessModelDetails, + { + [params.entityType]: params.entitySlug, + }, + { accessModelId: accessModelId } + ) ); const [accessModelData, setAccessModelData] = useState({ @@ -288,7 +310,13 @@ const AccessModelForm: React.FC = ({ const { mutate, isLoading: editMutationLoading } = useMutation( (data: { accessModelInput: EditAccessModelInput }) => - GraphQL(editaccessModel, data), + GraphQL( + editaccessModel, + { + [params.entityType]: params.entitySlug, + }, + data + ), { onSuccess: (res: any) => { // toast('Access Model Saved'); diff --git a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/AccessModelList.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx similarity index 84% rename from app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/AccessModelList.tsx rename to app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx index f90c5b89..3fe19f0a 100644 --- a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/AccessModelList.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx @@ -48,7 +48,11 @@ const AccessModelList: React.FC = ({ list, setAccessModelId, }) => { - const params = useParams(); + const params = useParams<{ + entityType: string; + entitySlug: string; + id: string; + }>(); const { data, @@ -57,9 +61,15 @@ const AccessModelList: React.FC = ({ }: { data: any; isLoading: boolean; refetch: any } = useQuery( [`accessModelList_${params.id}`], () => - GraphQL(accessModelQuery, { - datasetId: params.id, - }) + GraphQL( + accessModelQuery, + { + [params.entityType]: params.entitySlug, + }, + { + datasetId: params.id, + } + ) ); const [filteredRows, setFilteredRows] = useState([]); @@ -72,7 +82,14 @@ const AccessModelList: React.FC = ({ }, [data, list]); const { mutate, isLoading: deleteLoading } = useMutation( - (data: { accessModelId: UUID }) => GraphQL(deleteAccessModel, data), + (data: { accessModelId: UUID }) => + GraphQL( + deleteAccessModel, + { + [params.entityType]: params.entitySlug, + }, + data + ), { onSuccess: () => { toast('Access Model Deleted Successfully'); @@ -84,21 +101,23 @@ const AccessModelList: React.FC = ({ } ); + const handleAccessModel = (row: any) => { + setAccessModelId(row.original.id); + setList(false); + }; + const generateColumnData = () => { return [ { accessorKey: 'name', header: 'Name of Access Type', cell: ({ row }: any) => ( - { - setAccessModelId(row.original.id); - setList(false); - }} +
handleAccessModel(row)} > - {row.original.name} - + {row.original.name} +
), }, { diff --git a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditDataset.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditDataset.tsx similarity index 100% rename from app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditDataset.tsx rename to app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditDataset.tsx diff --git a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditDistribution.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditDistribution.tsx similarity index 100% rename from app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditDistribution.tsx rename to app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditDistribution.tsx diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditLayout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditLayout.tsx new file mode 100644 index 00000000..60ab2720 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditLayout.tsx @@ -0,0 +1,244 @@ +'use client'; + +import { ReactNode, useEffect, useState } from 'react'; +import { useParams, usePathname, useRouter } from 'next/navigation'; +import { graphql } from '@/gql'; +import { UpdateDatasetInput } from '@/gql/generated/graphql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { Tab, TabList, Tabs, toast } from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import TitleBar from '../../../../components/title-bar'; +import { useDatasetEditStatus } from '../context'; +import StepNavigation from '../../../../components/StepNavigation'; + +const datasetQueryDoc: any = graphql(` + query datasetTitleQuery($filters: DatasetFilter) { + datasets(filters: $filters) { + id + title + created + } + } +`); + +const updateDatasetTitleMutationDoc: any = graphql(` + mutation SaveTitle($updateDatasetInput: UpdateDatasetInput!) { + updateDataset(updateDatasetInput: $updateDatasetInput) { + __typename + ... on TypeDataset { + id + title + created + } + ... on OperationInfo { + messages { + kind + message + } + } + } + } +`); + +interface LayoutProps { + children?: ReactNode; + params: { id: string }; +} + +const layoutList = ['metadata', 'resources', 'publish']; + +export function EditLayout({ children, params }: LayoutProps) { + // const { data } = useQuery([`dataset_layout_${params.id}`], () => + // GraphQL(datasetQueryDoc, { dataset_id: Number(params.id) }) + // ); + + const pathName = usePathname(); + const routerParams = useParams<{ + entityType: string; + entitySlug: string; + id: string; + }>(); + + const [editMode, setEditMode] = useState(false); + + const getDatasetTitleRes: { data: any; isLoading: boolean; refetch: any } = + useQuery([`dataset_title_${routerParams.id}`], () => + GraphQL( + datasetQueryDoc, + { + [routerParams.entityType]: routerParams.entitySlug, + }, + { + filters: { + id: routerParams.id, + }, + } + ) + ); + + const updateDatasetTitleMutation = useMutation( + (data: { updateDatasetInput: UpdateDatasetInput }) => + GraphQL( + updateDatasetTitleMutationDoc, + { + [routerParams.entityType]: routerParams.entitySlug, + }, + data + ), + { + onSuccess: (data: any) => { + // queryClient.invalidateQueries({ + // queryKey: [`create_dataset_${'52'}`], + // }); + + setEditMode(false); + + getDatasetTitleRes.refetch(); + }, + onError: (err: any) => { + toast(err.message.split(':')[0]); + }, + } + ); + + const pathItem = layoutList.find(function (v) { + return pathName.indexOf(v) >= 0; + }); + + const { status, setStatus } = useDatasetEditStatus(); + + // if not from the layoutList, return children + if (!pathItem) { + return <>{children}; + } + + return ( +
+ {getDatasetTitleRes.isLoading ? ( + <> + ) : ( + + updateDatasetTitleMutation.mutate({ + updateDatasetInput: { + dataset: routerParams.id, + title: val, + }, + }) + } + loading={updateDatasetTitleMutation.isLoading} + status={status} + setStatus={setStatus} + /> + )} +
+
+ +
+
+ {children} +
+
+ +
+
+
+ ); +} + +const Navigation = ({ + id, + pathItem, + organization, + entityType, +}: { + id: string; + pathItem: string; + organization: string; + entityType: string; +}) => { + const router = useRouter(); + + let links = [ + { + label: 'Metadata', + id: 'metadata', + url: `/dashboard/${entityType}/${organization}/dataset/${id}/edit/metadata`, + // selected: pathItem === 'metadata', + }, + { + label: 'Data Files', + id: 'resources', + url: `/dashboard/${entityType}/${organization}/dataset/${id}/edit/resources`, + // selected: pathItem === 'resources', + }, + ...(process.env.NEXT_PUBLIC_ENABLE_ACCESSMODEL === 'true' + ? [ + { + label: 'Access Models', + id: 'access', + url: `/dashboard/${entityType}/${organization}/dataset/${id}/edit/access?list=true`, + // selected: pathItem === 'access', + }, + ] + : []), + // { + // label: 'Charts', + // id: 'charts', + // url: `/dashboard/${entityType}/${organization}/dataset/${id}/edit/charts?type=list`, + // // selected: pathItem === 'charts', + // }, + + { + label: 'Publish', + id: 'publish', + url: `/dashboard/${entityType}/${organization}/dataset/${id}/edit/publish`, + // selected: pathItem === 'publish', + }, + ]; + + const [selectedTab, setSelectedTab] = useState(pathItem || 'distributions'); + + const handleTabClick = (item: { + label: string; + url: string; + // selected: boolean; + }) => { + if (item.label !== selectedTab) { + setSelectedTab(item.label); + router.replace(item.url); + } + }; + + useEffect(() => { + setSelectedTab(pathItem); // Update selected tab on path change + }, [pathItem]); + + return ( +
+ + + {links.map((item, index) => ( + handleTabClick(item)} + > + {item.label} + + ))} + + +
+ ); +}; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx new file mode 100644 index 00000000..10639f6a --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx @@ -0,0 +1,617 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import { graphql } from '@/gql'; +import { + TypeDataset, + TypeMetadata, + TypeSector, + TypeTag, + UpdateMetadataInput, +} from '@/gql/generated/graphql'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + Checkbox, + Combobox, + Form, + FormLayout, + Input, + Select, + Text, + TextField, + toast, +} from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { Loading } from '@/components/loading'; +import DatasetLoading from '../../../components/loading-dataset'; +import { useDatasetEditStatus } from '../context'; + +const sectorsListQueryDoc: any = graphql(` + query SectorList { + sectors { + id + name + } + } +`); + +const tagsListQueryDoc: any = graphql(` + query TagsList { + tags { + id + value + } + } +`); + +const datasetMetadataQueryDoc: any = graphql(` + query MetadataValues($filters: DatasetFilter) { + datasets(filters: $filters) { + title + id + description + tags { + id + value + } + sectors { + id + name + } + license + metadata { + metadataItem { + id + label + dataType + } + id + value + } + accessType + } + } +`); + +const metadataQueryDoc: any = graphql(` + query MetaDataList($filters: MetadataFilter) { + metadata(filters: $filters) { + id + label + dataStandard + urn + dataType + options + validator + type + model + enabled + filterable + } + } +`); + +const updateMetadataMutationDoc: any = graphql(` + mutation SaveMetadata($UpdateMetadataInput: UpdateMetadataInput!) { + addUpdateDatasetMetadata(updateMetadataInput: $UpdateMetadataInput) { + success + errors { + fieldErrors { + field + messages + } + } + data { + id + description + title + tags { + id + value + } + sectors { + id + name + } + license + accessType + metadata { + metadataItem { + id + label + dataType + } + id + value + } + } + } + } +`); + +export function EditMetadata({ id }: { id: string }) { + const params = useParams<{ + entityType: string; + entitySlug: string; + id: string; + }>(); + + const queryClient = useQueryClient(); + + const getDatasetMetadata: { + data: any; + isLoading: boolean; + refetch: any; + error: any; + } = useQuery( + [`metadata_values_query_${params.id}`], + () => + GraphQL( + datasetMetadataQueryDoc, + { + [params.entityType]: params.entitySlug, + }, + { filters: { id: params.id } } + ), + { + refetchOnMount: true, + refetchOnReconnect: true, + } + ); + + const getSectorsList: { data: any; isLoading: boolean; error: any } = + useQuery([`sectors_list_query`], () => + GraphQL( + sectorsListQueryDoc, + { + [params.entityType]: params.entitySlug, + }, + [] + ) + ); + + const getTagsList: { data: any; isLoading: boolean; error: any } = useQuery( + [`tags_list_query`], + () => + GraphQL( + tagsListQueryDoc, + { + [params.entityType]: params.entitySlug, + }, + [] + ) + ); + + const getMetaDataListQuery: { + data: any; + isLoading: boolean; + refetch: any; + } = useQuery([`metadata_fields_list_${id}`], () => + GraphQL( + metadataQueryDoc, + { + [params.entityType]: params.entitySlug, + }, + { + filters: { + model: 'DATASET', + enabled: true, + }, + } + ) + ); + + const updateMetadataMutation = useMutation( + (data: { UpdateMetadataInput: UpdateMetadataInput }) => + GraphQL( + updateMetadataMutationDoc, + { + [params.entityType]: params.entitySlug, + }, + data + ), + { + onSuccess: (res: any) => { + if (res.addUpdateDatasetMetadata.success) { + toast('Details updated successfully!'); + queryClient.invalidateQueries({ + queryKey: [ + `metadata_values_query_${params.id}`, + `metadata_fields_list_${id}`, + ], + }); + const updatedData = defaultValuesPrepFn( + res.addUpdateDatasetMetadata.data + ); + setFormData(updatedData); + setPreviousFormData(updatedData); + // getDatasetMetadata.refetch(); + } else { + toast( + 'Error: ' + + res.addUpdateDatasetMetadata.errors.fieldErrors[0].messages[0] + ); + } + }, + } + ); + + const defaultValuesPrepFn = (dataset: TypeDataset) => { + let defaultVal: { + [key: string]: any; + } = {}; + + dataset?.metadata.length > 0 && + dataset?.metadata?.map((field) => { + if ( + field.metadataItem.dataType === 'MULTISELECT' && + field.value !== '' + ) { + defaultVal[field.metadataItem.id] = field.value + .split(', ') + .map((value: string) => ({ + label: value, + value: value, + })); + } else if (!field.value) { + defaultVal[field.metadataItem.id] = null; + } else { + defaultVal[field.metadataItem.id] = field.value; + } + }); + + defaultVal['description'] = dataset?.description || ''; + + defaultVal['sectors'] = + dataset?.sectors?.map((sector: TypeSector) => { + return { + label: sector.name, + value: sector.id, + }; + }) || []; + + defaultVal['license'] = dataset?.license || null; + + defaultVal['tags'] = + dataset?.tags?.map((tag: TypeTag) => { + return { + label: tag.value, + value: tag.id, + }; + }) || []; + + defaultVal['isPublic'] = true; + + return defaultVal; + }; + + const [formData, setFormData] = useState( + defaultValuesPrepFn(getDatasetMetadata?.data?.datasets[0]) + ); + const [previousFormData, setPreviousFormData] = useState(formData); + + useEffect(() => { + if (getDatasetMetadata.data?.datasets[0]) { + const updatedData = defaultValuesPrepFn( + getDatasetMetadata.data.datasets[0] + ); + setFormData(updatedData); + setPreviousFormData(updatedData); + } + }, [getDatasetMetadata.data]); + + const handleChange = (field: string, value: any) => { + setFormData((prevData) => ({ + ...prevData, + [field]: value, + })); + }; + + const handleSave = (updatedData: any) => { + if (JSON.stringify(updatedData) !== JSON.stringify(previousFormData)) { + setPreviousFormData(updatedData); + + const transformedValues = Object.keys(updatedData)?.reduce( + (acc: any, key) => { + acc[key] = Array.isArray(updatedData[key]) + ? updatedData[key].map((item: any) => item.value || item).join(', ') + : updatedData[key]; + return acc; + }, + {} + ); + + updateMetadataMutation.mutate({ + UpdateMetadataInput: { + dataset: id, + metadata: [ + ...Object.keys(transformedValues) + .filter( + (valueItem) => + ![ + 'sectors', + 'description', + 'tags', + 'isPublic', + 'license', + ].includes(valueItem) && transformedValues[valueItem] !== '' + ) + .map((key) => { + return { + id: key, + value: transformedValues[key], + }; + }), + ], + ...(updatedData.license && { license: updatedData.license }), + accessType: updatedData.accessType || 'PUBLIC', + description: updatedData.description || '', + tags: updatedData.tags?.map((item: any) => item.label) || [], + sectors: updatedData.sectors?.map((item: any) => item.value) || [], + }, + }); + } + }; + + function renderInputField(metadataFormItem: any) { + if (metadataFormItem.dataType === 'STRING') { + return ( +
+ handleChange(metadataFormItem.id, e)} + onBlur={() => handleSave(formData)} // Save on blur + /> +
+ ); + } + + if (metadataFormItem.dataType === 'SELECT') { + return ( +
+ ({ + label: option, + value: option, + }))} + label={metadataFormItem.label} + displaySelected + onChange={(value) => { + handleChange(metadataFormItem.id, value); + handleSave({ ...formData, [metadataFormItem.id]: value }); // Save on change + }} + /> +
+ ); + } + + if (metadataFormItem.dataType === 'MULTISELECT') { + const prefillData = metadataFormItem.value ? metadataFormItem.value : []; + + return ( +
+ ({ + label: option, + value: option, + })) || []), + ]} + label={metadataFormItem.label} + displaySelected + selectedValue={prefillData} + onChange={(value) => { + handleChange(metadataFormItem.id, value); + handleSave({ ...formData, [metadataFormItem.id]: value }); // Save on change + }} + /> +
+ ); + } + if (metadataFormItem.dataType === 'URL') { + return ( +
+ handleChange(metadataFormItem.id, e)} + onBlur={() => handleSave(formData)} // Save on blur + /> +
+ ); + } + + if (metadataFormItem.dataType === 'DATE') { + return ( +
+ handleChange(metadataFormItem.id, e)} + onBlur={() => handleSave(formData)} // Save on blur + /> +
+ ); + } + + // Add more conditions for other data types as needed + return null; + } + + const licenseOptions = [ + { + label: 'Government Open Data License', + value: 'GOVERNMENT_OPEN_DATA_LICENSE', + }, + { + label: 'CC BY 4.0 (Attribution)', + value: 'CC_BY_4_0_ATTRIBUTION', + }, + { + label: 'CC BY-SA 4.0 (Attribution-ShareAlike)', + value: 'CC_BY_SA_4_0_ATTRIBUTION_SHARE_ALIKE', + }, + { + label: 'Open Data Commons By Attribution', + value: 'OPEN_DATA_COMMONS_BY_ATTRIBUTION', + }, + { + label: 'Open Database License', + value: 'OPEN_DATABASE_LICENSE', + }, + ]; + + const { setStatus } = useDatasetEditStatus(); + + useEffect(() => { + setStatus(updateMetadataMutation.isLoading ? 'loading' : 'success'); // update based on mutation state + }, [updateMetadataMutation.isLoading]); + + return ( + <> + {!getTagsList?.isLoading && + !getSectorsList?.isLoading && + !getDatasetMetadata.isLoading ? ( +
+ <> + +
+
+ handleChange('description', e)} + onBlur={() => handleSave(formData)} // Save on blur + /> +
+ + { + return { label: item.name, value: item.id }; + } + )} + name="sectors" + onChange={(value) => { + handleChange('sectors', value); + handleSave({ ...formData, sectors: value }); // Save on change + }} + /> + { + return { + label: item.value, + value: item.id, + }; + })} + label="Tags *" + creatable + onChange={(value) => { + handleChange('tags', value); + handleSave({ ...formData, tags: value }); // Save on change + }} + /> +
+
+ {getMetaDataListQuery?.data?.metadata + ?.filter( + (item: TypeMetadata) => item.dataType === 'MULTISELECT' + ) + .map((item: TypeMetadata) => ( +
{renderInputField(item)}
+ ))} +
+ {getMetaDataListQuery?.data?.metadata + ?.filter( + (item: TypeMetadata) => item.dataType !== 'MULTISELECT' + ) + .map((item: TypeMetadata) => renderInputField(item))} +
+
+ +
+
+ handleChange('accessType', 'PUBLIC')} + > +
+ Open Access + + Dataset can be viewed and downloaded by everyone + +
+
+ +
+ Restricted Access + + Users would require to request access to the dataset to + view and download it. Recommended for sensitive data. + +
+
+
+ + setFormData({ + ...formData, + organizationTypes: e as ApiOrganizationOrganizationTypesEnum, + }) + } + options={organizationTypes} + /> +
+
+ setFormData({ ...formData, homepage: e })} + /> +
+
+
+
+ setFormData({ ...formData, linkedinProfile: e })} + /> +
+ +
+ setFormData({ ...formData, githubProfile: e })} + /> +
+
+
+
+ setFormData({ ...formData, twitterProfile: e })} + /> +
+ +
+ setFormData({ ...formData, location: e })} + /> +
+
+
+
+ setFormData({ ...formData, description: e })} + /> +
+
+ setFormData({ ...formData, logo: e[0] })} + name={'Logo'} + > + + +
+
+ + + + ); +}; + +export default OrgProfile; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/page.tsx new file mode 100644 index 00000000..56e095e0 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/page.tsx @@ -0,0 +1,19 @@ +'use client'; + +import React from 'react'; +import { usePathname } from 'next/navigation'; + +import OrgProfile from './orgProfile'; +import UserProfile from './userProfile'; + +const Profile = () => { + const path = usePathname(); + + return ( +
+ {path.includes('self') ? : } +
+ ); +}; + +export default Profile; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/userProfile.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/userProfile.tsx new file mode 100644 index 00000000..8ca80e9a --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/profile/userProfile.tsx @@ -0,0 +1,237 @@ +'use client'; + +import React, { useEffect } from 'react'; +import { useParams } from 'next/navigation'; +import { graphql } from '@/gql'; +import { UpdateUserInput } from '@/gql/generated/graphql'; +import { useMutation } from '@tanstack/react-query'; +import { Button, DropZone, Text, TextField, toast } from 'opub-ui'; + +import { useDashboardStore } from '@/config/store'; +import { GraphQL } from '@/lib/api'; + +const updateUserMutation: any = graphql(` + mutation updateUser($input: UpdateUserInput!) { + updateUser(input: $input) { + __typename + ... on TypeUser { + id + firstName + lastName + email + bio + profilePicture { + name + path + url + } + username + githubProfile + linkedinProfile + twitterProfile + location + } + } + } +`); + +const UserProfile = () => { + const params = useParams<{ entitySlug: string }>(); + + const { setUserDetails, userDetails } = useDashboardStore(); + + useEffect(() => { + if (userDetails && userDetails?.me) { + setFormData({ + firstName: userDetails?.me?.firstName, + lastName: userDetails?.me?.lastName, + email: userDetails?.me?.email, + bio: userDetails?.me?.bio, + profilePicture: userDetails?.me?.profilePicture, + githubProfile: userDetails?.me?.githubProfile, + linkedinProfile: userDetails?.me?.linkedinProfile, + twitterProfile: userDetails?.me?.twitterProfile, + location: userDetails?.me?.location, + }); + } + }, [userDetails]); + + const initialFormData = { + firstName: '', + lastName: '', + email: '', + bio: '', + profilePicture: null as File | null, + githubProfile: '', + linkedinProfile: '', + twitterProfile: '', + location: '', + }; + + const { mutate, isLoading: editMutationLoading } = useMutation( + (input: { input: UpdateUserInput }) => + GraphQL(updateUserMutation, {}, input), + { + onSuccess: (res: any) => { + toast('User details updated successfully'); + setFormData({ + firstName: res?.updateUser?.firstName, + lastName: res?.updateUser?.lastName, + email: res?.updateUser?.email, + bio: res?.updateUser?.bio, + profilePicture: res?.updateUser?.profilePicture, + githubProfile: res?.updateUser?.githubProfile, + linkedinProfile: res?.updateUser?.linkedinProfile, + twitterProfile: res?.updateUser?.twitterProfile, + location: res?.updateUser?.location, + }); + setUserDetails({ + ...userDetails, + me: res.updateUser, + }); + }, + onError: (error: any) => { + toast(`Error: ${error.message}`); + }, + } + ); + + const [formData, setFormData] = React.useState(initialFormData); + + const handleSave = () => { + // Create mutation input with only changed fields + const formValidation = + formData.firstName && + formData.lastName && + formData.email && + formData.bio && + formData.profilePicture; + + + if (!formValidation) { + toast('Please fill all the required fields'); + return; + } else { + const inputData: UpdateUserInput = { + firstName: formData.firstName, + lastName: formData.lastName, + bio: formData.bio, + email: formData.email, + githubProfile: formData.githubProfile, + linkedinProfile: formData.linkedinProfile, + twitterProfile: formData.twitterProfile, + location: formData.location, + }; + + // Only add logo if it has changed + if (formData.profilePicture instanceof File) { + inputData.profilePicture = formData.profilePicture; + } + mutate({ input: inputData }); + } + }; + + return ( +
+
+ My Profile +
+
+
+
+
+
+ setFormData({ ...formData, firstName: e })} + /> +
+
+ setFormData({ ...formData, lastName: e })} + /> +
+
+ +
+ setFormData({ ...formData, email: e })} + /> +
+
+ setFormData({ ...formData, location: e })} + /> +
+
+
+ setFormData({ ...formData, githubProfile: e })} + /> + setFormData({ ...formData, linkedinProfile: e })} + /> + setFormData({ ...formData, twitterProfile: e })} + /> +
+
+
+
+ setFormData({ ...formData, bio: e })} + /> +
+
+ setFormData({ ...formData, profilePicture: e[0] })} + name={'Profile Picture'} + > + + +
+
+ + +
+
+ ); +}; + +export default UserProfile; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/schema.ts b/app/[locale]/dashboard/[entityType]/[entitySlug]/schema.ts new file mode 100644 index 00000000..0ae657fb --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/schema.ts @@ -0,0 +1,58 @@ +import { graphql } from '@/gql'; + +export const getOrgDetailsQryDoc: any = graphql(` + query getOrgDetailsQry($slug: String) { + organizations(slug: $slug) { + id + name + logo { + name + path + size + url + width + height + } + homepage + organizationTypes + contactEmail + description + slug + githubProfile + linkedinProfile + twitterProfile + location + } + } +`); + +export const UserDetailsQryDoc: any = graphql(` + query userDetails { + me { + bio + email + firstName + lastName + profilePicture { + name + path + url + } + username + githubProfile + linkedinProfile + twitterProfile + location + id + organizationMemberships { + organization { + name + id + } + role { + name + } + } + } + } +`); \ No newline at end of file diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/assign/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/assign/page.tsx new file mode 100644 index 00000000..3c325145 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/assign/page.tsx @@ -0,0 +1,171 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { fetchDatasets } from '@/fetch'; +import { graphql } from '@/gql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { Button, DataTable, Text, toast } from 'opub-ui'; + +import { GraphQL } from '@/lib/api'; +import { formatDate } from '@/lib/utils'; +import { Loading } from '@/components/loading'; + +const FetchUseCaseDetails: any = graphql(` + query UseCaseDetails($filters: UseCaseFilter) { + useCases(filters: $filters) { + id + title + datasets { + id + title + modified + sectors { + name + } + } + } + } +`); + +const AssignUsecaseDatasets: any = graphql(` + mutation assignDatasets($useCaseId: String!, $datasetIds: [UUID!]!) { + updateUsecaseDatasets(useCaseId: $useCaseId, datasetIds: $datasetIds) { + ... on TypeUseCase { + id + datasets { + id + title + } + } + } + } +`); +const Assign = () => { + const params = useParams(); + const router = useRouter(); + + const [data, setData] = useState([]); // Ensure `data` is an array + const [selectedRow, setSelectedRows] = useState([]); + + const UseCaseDetails: { data: any; isLoading: boolean; refetch: any } = + useQuery( + [`UseCase_Details`, params.id], + () => + GraphQL( + FetchUseCaseDetails, + {}, + { + filters: { + id: params.id, + }, + } + ), + { + refetchOnMount: true, + refetchOnReconnect: true, + } + ); + + const formattedData = (data: any) => + data.map((item: any) => { + return { + title: item.title, + id: item.id, + category: item.sectors[0]?.name || 'N/A', // Safeguard in case of missing category + modified: formatDate(item.modified), + }; + }); + + useEffect(() => { + fetchDatasets('?size=1000&page=1') + .then((res) => { + setData(res.results); + }) + .catch((err) => { + console.error(err); + }); + }, []); + + + + const columns = [ + { accessorKey: 'title', header: 'Title' }, + { accessorKey: 'category', header: 'Sector' }, + { accessorKey: 'modified', header: 'Last Modified' }, + ]; + + + const generateTableData = (list: Array) => { + return list.map((item) => { + return { + title: item.title, + id: item.id, + category: item.sectors[0], + modified: formatDate(item.modified), + }; + }); + }; + + const { mutate, isLoading: mutationLoading } = useMutation( + () => + GraphQL( + AssignUsecaseDatasets, + {}, + { + useCaseId: params.id, + datasetIds: Array.isArray(selectedRow) + ? selectedRow.map((row: any) => row.id) + : [], + } + ), + { + onSuccess: (data: any) => { + toast('Dataset Assigned Successfully'); + UseCaseDetails.refetch(); + router.push(`/dashboard/${params.entityType}/${params.entitySlug}/usecases/edit/${params.id}/contributors`); + }, + onError: (err: any) => { + toast(`Received ${err} on dataset publish `); + }, + } + ); + + return ( + <> + {UseCaseDetails?.data?.useCases[0]?.datasets?.length >= 0 && + data.length > 0 && + !UseCaseDetails.isLoading ? ( + <> +
+
+ + Selected {selectedRow.length} of {data.length} + +
+
+ +
+
+ + { + setSelectedRows(Array.isArray(selected) ? selected : []); // Ensure selected is always an array + }} + /> + + ) : ( + + )} + + ); +}; + +export default Assign; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/CustomCombobox.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/CustomCombobox.tsx new file mode 100644 index 00000000..cd83ff31 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/CustomCombobox.tsx @@ -0,0 +1,79 @@ +import { useState } from 'react'; + +type Option = { + label: string; + value: string; +}; + +type CustomComboboxProps = { + options: Option[]; + selectedValue: Option[]; + onChange: (value: Option[]) => void; + onInput?: (value: string) => void; + placeholder?: string; +}; + + +const CustomCombobox: React.FC = ({ + options, + selectedValue, + onChange, + onInput, + placeholder, +}) => { + const [searchValue, setSearchValue] = useState(''); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const filteredOptions = options.filter( + (option) => + !selectedValue.some((selected) => selected.value === option.value) && + option.label.toLowerCase().includes(searchValue.toLowerCase()) + ); + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchValue(value); + onInput?.(value); // Pass the input value to parent component + setIsDropdownOpen(true); // Keep dropdown open while typing + }; + + const handleSelectOption = (option: Option) => { + // Prevent adding duplicate values + if (selectedValue.some((item) => item.value === option.value)) { + return; + } + // Add the selected option to the list + onChange([...selectedValue, option]); + setSearchValue(''); // Clear input after selection + setIsDropdownOpen(false); // Close dropdown + }; + + return ( +
+ + {isDropdownOpen && filteredOptions.length > 0 && ( +
+ {filteredOptions.map((option) => ( +
handleSelectOption(option)} + > + {option.label} +
+ ))} +
+ )} +
+ ); +}; + +export default CustomCombobox; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/EntitySelection.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/EntitySelection.tsx new file mode 100644 index 00000000..bd38ff4f --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/EntitySelection.tsx @@ -0,0 +1,86 @@ +import Image from 'next/image'; +import { Button, Icon, Text } from 'opub-ui'; + +import { Icons } from '@/components/icons'; +import CustomCombobox from './CustomCombobox'; + +type Option = { label: string; value: string }; + +type EntitySectionProps = { + title: string; + label: string; + placeholder: string; + options: Option[]; + selectedValues: Option[]; + onChange: (values: Option[]) => void; + onRemove: (value: Option) => void; + data: any; +}; + +const EntitySection = ({ + title, + label, + placeholder, + options, + selectedValues, + onChange, + onRemove, + data, +}: EntitySectionProps) => ( +
+ {title} +
+
+
+ {label} + + onChange([ + ...selectedValues, + ...value.filter( + (val) => + !selectedValues.some((item) => item.value === val.value) + ), + ]) + } + /> +
+
+ {selectedValues.map((item) => ( +
+
+ org.id === item.value)?.logo?.url + ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${ + data?.find((org: any) => org.id === item.value)?.logo + ?.url + }` + : '/org.png' + } + alt={item.label} + width={140} + height={100} + className="object-contain" + /> +
+ +
+ ))} +
+
+
+
+); + +export default EntitySection; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/PartnerModal.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/PartnerModal.tsx new file mode 100644 index 00000000..df20bad3 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/PartnerModal.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Dialog,Button,TextField,DropZone } from 'opub-ui'; + +type ModalProps = { + + setIsModalOpen?: (value: boolean) => void; + isModalOpen?: false; + }; + + + const PartnerModal: React.FC = ({isModalOpen,setIsModalOpen}) => { + return ( +
+ {' '} + setIsModalOpen?.(open)}> + + + + +
+
+ console.log(e)} + /> +
+
+ console.log(e)} + /> +
+
+ console.log(e)} + name={'Logo'} + > + + +
+ +
+
+
{' '} +
+ ); +}; + +export default PartnerModal; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/page.tsx new file mode 100644 index 00000000..f668168e --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/page.tsx @@ -0,0 +1,437 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import { useParams } from 'next/navigation'; +import { graphql } from '@/gql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { Button, Icon, Text, toast } from 'opub-ui'; + +import { useDashboardStore } from '@/config/store'; +import { GraphQL } from '@/lib/api'; +import { Icons } from '@/components/icons'; +import { Loading } from '@/components/loading'; +import { useEditStatus } from '../../context'; +import CustomCombobox from './CustomCombobox'; +import EntitySection from './EntitySelection'; +import { + AddContributors, + AddPartners, + AddSupporters, + FetchUsecaseInfo, + FetchUsers, + RemoveContributor, + RemovePartners, + RemoveSupporters, +} from './query'; + +const Details = () => { + const params = useParams<{ id: string }>(); + const { allEntityDetails } = useDashboardStore(); + const [searchValue, setSearchValue] = useState(''); + const [formData, setFormData] = useState({ + contributors: [] as { label: string; value: string }[], + supporters: [] as { label: string; value: string }[], + partners: [] as { label: string; value: string }[], + }); + + const Users: { data: any; isLoading: boolean; refetch: any } = useQuery( + [`fetch_users`], + () => + GraphQL( + FetchUsers, + {}, + { + limit: 10, + searchTerm: searchValue, + } + ), + { + enabled: searchValue.length > 0, + keepPreviousData: true, + } + ); + + const UseCaseData: { data: any; isLoading: boolean; refetch: any } = useQuery( + [`fetch_usecase_${params.id}`], + () => + GraphQL( + FetchUsecaseInfo, + {}, + { + filters: { + id: params.id, + }, + } + ), + { + refetchOnMount: true, + refetchOnReconnect: true, + } + ); + + useEffect(() => { + setFormData((prev) => ({ + ...prev, + partners: + UseCaseData?.data?.useCases?.[0]?.partnerOrganizations?.map( + (org: any) => ({ + label: org.name, + value: org.id, + }) + ) || [], + supporters: + UseCaseData?.data?.useCases?.[0]?.supportingOrganizations?.map( + (org: any) => ({ + label: org.name, + value: org.id, + }) + ) || [], + contributors: + UseCaseData?.data?.useCases?.[0]?.contributors?.map((user: any) => ({ + label: user.fullName, + value: user.id, + })) || [], + })); + }, [UseCaseData?.data]); + + const { mutate: addContributor, isLoading: addContributorLoading } = + useMutation( + (input: { useCaseId: string; userId: string }) => + GraphQL(AddContributors, {}, input), + { + onSuccess: (res: any) => { + toast('Contributor added successfully'); + UseCaseData.refetch(); + }, + onError: (error: any) => { + toast(`Error: ${error.message}`); + }, + } + ); + + const { mutate: removeContributor, isLoading: removeContributorLoading } = + useMutation( + (input: { useCaseId: string; userId: string }) => + GraphQL(RemoveContributor, {}, input), + { + onSuccess: (res: any) => { + toast('Contributor removed successfully'); + }, + onError: (error: any) => { + toast(`Error: ${error.message}`); + }, + } + ); + + const { mutate: addSupporter, isLoading: addSupporterLoading } = useMutation( + (input: { useCaseId: string; organizationId: string }) => + GraphQL(AddSupporters, {}, input), + { + onSuccess: (res: any) => { + toast('Supporter added successfully'); + UseCaseData.refetch(); + }, + onError: (error: any) => { + toast(`Error: ${error.message}`); + }, + } + ); + + const { mutate: removeSupporter, isLoading: removeSupporterLoading } = + useMutation( + (input: { useCaseId: string; organizationId: string }) => + GraphQL(RemoveSupporters, {}, input), + { + onSuccess: (res: any) => { + toast('Supporter removed successfully'); + }, + onError: (error: any) => { + toast(`Error: ${error.message}`); + }, + } + ); + + const { mutate: addPartner, isLoading: addPartnerLoading } = useMutation( + (input: { useCaseId: string; organizationId: string }) => + GraphQL(AddPartners, {}, input), + { + onSuccess: (res: any) => { + toast('Partner added successfully'); + UseCaseData.refetch(); + }, + onError: (error: any) => { + toast(`Error: ${error.message}`); + }, + } + ); + + const { mutate: removePartner, isLoading: removePartnerLoading } = + useMutation( + (input: { useCaseId: string; organizationId: string }) => + GraphQL(RemovePartners, {}, input), + { + onSuccess: (res: any) => { + toast('Partner removed successfully'); + }, + onError: (error: any) => { + toast(`Error: ${error.message}`); + }, + } + ); + + useEffect(() => { + Users.refetch(); + }, [searchValue]); + + const selectedContributors = formData.contributors; + + const options = + Users?.data?.searchUsers?.map((user: any) => ({ + label: user.fullName, + value: user.id, + })) || []; + + const { setStatus } = useEditStatus(); + + const loadingStates = [ + addContributorLoading, + removeContributorLoading, + addSupporterLoading, + removeSupporterLoading, + addPartnerLoading, + removePartnerLoading, + ]; + + useEffect(() => { + setStatus(loadingStates.some(Boolean) ? 'loading' : 'success'); + }, loadingStates); + + return ( +
+ {Users?.isLoading || allEntityDetails?.organizations?.length === 0 ? ( + + ) : ( +
+
+ CONTRIBUTORS +
+
+
+ Add Contributors + { + const prevValues = formData.contributors.map( + (item) => item.value + ); + const newlyAdded = newValues.find( + (item: any) => !prevValues.includes(item.value) + ); + + setFormData((prev) => ({ + ...prev, + contributors: newValues, + })); + + if (newlyAdded) { + addContributor({ + useCaseId: params.id, + userId: newlyAdded.value, + }); + } + setSearchValue(''); // clear input + }} + placeholder="Add Contributors" + onInput={(value: any) => { + setSearchValue(value); + }} + /> +
+
+ {formData.contributors.map((item) => ( +
+ contributor.id === item.value + )?.profilePicture?.url + ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${ + UseCaseData.data.useCases[0]?.contributors?.find( + (contributor: any) => + contributor.id === item.value + )?.profilePicture?.url + }` + : '/profile.png' + } + alt={item.label} + width={80} + height={80} + className="rounded-full object-cover" + /> + +
+ ))} +
+
+
{' '} +
+ + ({ + label: org.name, + value: org.id, + }) + )} + selectedValues={formData.supporters} + onChange={(newValues: any) => { + const prevValues = formData.supporters.map((item) => item.value); + const newlyAdded = newValues.find( + (item: any) => !prevValues.includes(item.value) + ); + + setFormData((prev) => ({ ...prev, supporters: newValues })); + + if (newlyAdded) { + addSupporter({ + useCaseId: params.id, + organizationId: newlyAdded.value, + }); + } + }} + onRemove={(item: any) => { + setFormData((prev) => ({ + ...prev, + supporters: prev.supporters.filter( + (s) => s.value !== item.value + ), + })); + removeSupporter({ + useCaseId: params.id, + organizationId: item.value, + }); + }} + /> + + ({ + label: org.name, + value: org.id, + }) + )} + selectedValues={formData.partners} + onChange={(newValues: any) => { + const prevValues = formData.partners.map((item) => item.value); + const newlyAdded = newValues.find( + (item: any) => !prevValues.includes(item.value) + ); + + setFormData((prev) => ({ ...prev, partners: newValues })); + + if (newlyAdded) { + addPartner({ + useCaseId: params.id, + organizationId: newlyAdded.value, + }); + } + }} + onRemove={(item: any) => { + setFormData((prev) => ({ + ...prev, + partners: prev.partners.filter((s) => s.value !== item.value), + })); + removePartner({ + useCaseId: params.id, + organizationId: item.value, + }); + }} + /> +
+ )} +
+ ); +}; + +export default Details; + +{ + /*
+
+ { + setFormData((prev) => ({ + ...prev, + contributors: [...prev.contributors, ...value], + })); + setSearchValue(''); // clear input + }} + onInput={(value: any) => { + console.log(value); + setSearchValue(value); + }} + key={Users?.data?.searchUsers?.length} + /> + + (Some Contributors have been preselected from added Datasets) + +
+
*/ +} +{ + /* ({ + label: org.name, + value: org.name, + })) + } + selectedValue={formData.partners} + onChange={(value: any) => { + setFormData((prev) => ({ + ...prev, + partners: value, + })); + }} + /> */ +} diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/query.ts b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/query.ts new file mode 100644 index 00000000..f9ba6794 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/contributors/query.ts @@ -0,0 +1,175 @@ +import { graphql } from '@/gql'; + + +export const FetchUsers: any = graphql(` + query searchUsers($limit: Int!, $searchTerm: String!) { + searchUsers(limit: $limit, searchTerm: $searchTerm) { + id + fullName + username + } + } + `); + + export const FetchUsecaseInfo: any = graphql(` + query useCaseinfo($filters: UseCaseFilter) { + useCases(filters: $filters) { + id + title + contributors { + id + fullName + username + profilePicture { + url + } + } + supportingOrganizations { + id + name + logo { + url + name + } + } + partnerOrganizations{ + id + name + logo{ + url + name + } + } + } + } + `); + + export const AddContributors: any = graphql(` + mutation addContributorToUseCase($useCaseId: String!, $userId: ID!) { + addContributorToUseCase(useCaseId: $useCaseId, userId: $userId) { + __typename + ... on TypeUseCase { + id + title + contributors { + id + fullName + username + } + } + } + } + `); + + export const RemoveContributor: any = graphql(` + mutation removeContributorFromUseCase($useCaseId: String!, $userId: ID!) { + removeContributorFromUseCase(useCaseId: $useCaseId, userId: $userId) { + __typename + ... on TypeUseCase { + id + title + contributors { + id + fullName + username + } + } + } + } + `); + + export const AddSupporters: any = graphql(` + mutation addSupportingOrganizationToUseCase( + $useCaseId: String! + $organizationId: ID! + ) { + addSupportingOrganizationToUseCase( + useCaseId: $useCaseId + organizationId: $organizationId + ) { + __typename + ... on TypeUseCaseOrganizationRelationship { + organization { + id + name + logo { + url + name + } + } + } + } + } + `); + + export const RemoveSupporters: any = graphql(` + mutation removeSupportingOrganizationFromUseCase( + $useCaseId: String! + $organizationId: ID! + ) { + removeSupportingOrganizationFromUseCase( + useCaseId: $useCaseId + organizationId: $organizationId + ) { + __typename + ... on TypeUseCaseOrganizationRelationship { + organization { + id + name + logo { + url + name + } + } + } + } + } + `); + + export const AddPartners: any = graphql(` + mutation addPartnerOrganizationToUseCase( + $useCaseId: String! + $organizationId: ID! + ) { + addPartnerOrganizationToUseCase( + useCaseId: $useCaseId + organizationId: $organizationId + ) { + __typename + ... on TypeUseCaseOrganizationRelationship { + organization { + id + name + logo { + url + name + } + } + } + } + } + `); + + export const RemovePartners: any = graphql(` + mutation removePartnerOrganizationFromUseCase( + $useCaseId: String! + $organizationId: ID! + ) { + removePartnerOrganizationFromUseCase( + useCaseId: $useCaseId + organizationId: $organizationId + ) { + __typename + ... on TypeUseCaseOrganizationRelationship { + organization { + id + name + logo { + url + name + } + } + } + } + } + `); \ No newline at end of file diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/details/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/details/page.tsx new file mode 100644 index 00000000..3253c56e --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/details/page.tsx @@ -0,0 +1,316 @@ +'use client'; + +import React, { useCallback, useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { graphql } from '@/gql'; +import { UseCaseInputPartial } from '@/gql/generated/graphql'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { DropZone, Select, TextField, toast } from 'opub-ui'; + +// Assuming you are using these components + +import { GraphQL } from '@/lib/api'; +import { useEditStatus } from '../../context'; +import Metadata from '../metadata/page'; + +const UpdateUseCaseMutation: any = graphql(` + mutation updateUseCase($data: UseCaseInputPartial!) { + updateUseCase(data: $data) { + __typename + id + title + summary + created + modified + website + runningStatus + slug + status + startedOn + completedOn + logo { + name + path + url + } + } + } +`); + +const FetchUseCase: any = graphql(` + query UseCaseData($filters: UseCaseFilter) { + useCases(filters: $filters) { + id + title + summary + website + logo { + name + path + url + } + runningStatus + contactEmail + status + slug + startedOn + completedOn + } + } +`); + +const Details = () => { + const params = useParams<{ + entityType: string; + entitySlug: string; + id: string; + }>(); + + const router = useRouter(); + + const UseCaseData: { data: any; isLoading: boolean; refetch: any } = useQuery( + [`fetch_UseCaseData`], + () => + GraphQL( + FetchUseCase, + {}, + { + filters: { + id: params.id, + }, + } + ), + { + refetchOnMount: true, + refetchOnReconnect: true, + } + ); + + const UsecasesData = + UseCaseData?.data?.useCases.length > 0 && UseCaseData?.data?.useCases[0]; + + const initialFormData = { + title: '', + summary: '', + logo: null as File | null, + website: '', + contactEmail: '', + slug: '', + status: '', + runningStatus: null, + startedOn: null, + completedOn: null, + }; + + const runningStatus = [ + { + label: 'Intitated', + value: 'INITIATED', + }, + { + label: 'On Going', + value: 'ON_GOING', + }, + { + label: 'Completed', + value: 'COMPLETED', + }, + { + label: 'Cancelled', + value: 'CANCELLED', + }, + ]; + + const [formData, setFormData] = useState(initialFormData); + + const [previousFormData, setPreviousFormData] = useState(initialFormData); + + useEffect(() => { + if (UsecasesData) { + // Ensure UsecasesData is available + const updatedData = { + title: UsecasesData.title || '', // Fallback to empty string if undefined + summary: UsecasesData.summary || '', + logo: UsecasesData.logo || null, + website: UsecasesData.website || '', + contactEmail: UsecasesData.contactEmail || '', + slug: UsecasesData.slug || '', + status: UsecasesData.status || '', + runningStatus: UsecasesData.runningStatus || null, + startedOn: UsecasesData.startedOn || '', + completedOn: UsecasesData.completedOn || '', + }; + setFormData(updatedData); + setPreviousFormData(updatedData); + } + }, [params.id, UsecasesData]); + + const { mutate, isLoading: editMutationLoading } = useMutation( + (data: { data: UseCaseInputPartial }) => + GraphQL(UpdateUseCaseMutation, {}, data), + { + onSuccess: (res: any) => { + toast('Use case updated successfully'); + setFormData((prev) => ({ + ...prev, + ...res.updateUseCase, + })); + setPreviousFormData((prev) => ({ + ...prev, + ...res.updateUseCase, + })); + }, + onError: (error: any) => { + toast(`Error: ${error.message}`); + }, + } + ); + + const handleChange = useCallback((field: string, value: any) => { + setFormData((prevData) => ({ + ...prevData, + [field]: value, + })); + }, []); + + const onDrop = React.useCallback( + (_dropFiles: File[], acceptedFiles: File[]) => { + mutate({ + data: { + id: params.id.toString(), + logo: acceptedFiles[0], + }, + }); + }, + [] + ); + + const handleSave = (updatedData: any) => { + if (JSON.stringify(updatedData) !== JSON.stringify(previousFormData)) { + setPreviousFormData(updatedData); + + mutate({ + data: { + id: params.id.toString(), + title: updatedData.title, + summary: updatedData.summary, + website: updatedData.website, + contactEmail: updatedData.contactEmail, + runningStatus: updatedData.runningStatus, + startedOn: (updatedData.startedOn as Date) || null, + completedOn: (updatedData.completedOn as Date) || null, + }, + }); + } + }; + const { setStatus } = useEditStatus(); + + useEffect(() => { + setStatus(editMutationLoading ? 'loading' : 'success'); // update based on mutation state + }, [editMutationLoading]); + + return ( +
+
+ handleChange('summary', e)} + onBlur={() => handleSave(formData)} + /> +
+ + +
+
+ { + handleChange('startedOn', e); + }} + onBlur={() => handleSave(formData)} + /> +
+ +
+ + setFormData({ + ...formData, + organizationTypes: + e as ApiOrganizationOrganizationTypesEnum, + }) + } + options={organizationTypes} + /> +
+
+ + setFormData({ ...formData, homepage: e }) + } + /> +
+
+ + setFormData({ ...formData, contactEmail: e }) + } + /> +
+ + setFormData({ ...formData, logo: e[0] }) + } + // onDrop={(e) => console.log(e)} + name={'Logo'} + > + + + + setFormData({ ...formData, linkedinProfile: e }) + } + /> + + setFormData({ ...formData, githubProfile: e }) + } + /> + + setFormData({ ...formData, twitterProfile: e }) + } + /> + + setFormData({ ...formData, location: e }) + } + /> + +
+ + + )} + +
+ + + )} + + + ); +}; + +export default Page; + +const EntityCard = ({ entityItem, params }: any) => { + const [isImageValid, setIsImageValid] = useState(() => { + return entityItem?.logo ? true : false; + }); + + return ( +
+
+ +
+ {isImageValid ? ( + {`${entityItem.name} { + setIsImageValid(false); + }} + className="object-contain" + /> + ) : ( + {`fallback + )} +
+ +
+
+ + {entityItem.name} + +
+
+ ); +}; diff --git a/app/[locale]/dashboard/[entityType]/schema.ts b/app/[locale]/dashboard/[entityType]/schema.ts new file mode 100644 index 00000000..4bd06091 --- /dev/null +++ b/app/[locale]/dashboard/[entityType]/schema.ts @@ -0,0 +1,67 @@ +import { graphql } from '@/gql'; + +export const allOrganizationsListingDoc: any = graphql(` + query allOrganizationsListingDoc { + organizations { + id + name + githubProfile + linkedinProfile + twitterProfile + location + logo { + name + path + size + url + width + height + } + slug + } + } +`); + +export const allDataSpacesListingDoc: any = graphql(` + query AllDataSpacesListDoc { + dataspaces { + id + name + logo { + name + path + size + url + width + height + } + slug + } + } +`); + +export const organizationCreationMutation: any = graphql(` + mutation createOrganization($input: OrganizationInput!) { + createOrganization(input: $input) { + __typename + ... on TypeOrganization { + id + name + logo { + name + path + url + } + homepage + organizationTypes + contactEmail + description + slug + githubProfile + linkedinProfile + twitterProfile + location + } + } + } +`); diff --git a/app/[locale]/dashboard/components/GraphqlPagination/footer.tsx b/app/[locale]/dashboard/components/GraphqlPagination/footer.tsx index fff4d6e7..9c4b5a0f 100644 --- a/app/[locale]/dashboard/components/GraphqlPagination/footer.tsx +++ b/app/[locale]/dashboard/components/GraphqlPagination/footer.tsx @@ -7,7 +7,8 @@ import { } from '@tabler/icons-react'; import { IconButton, Select, Text } from 'opub-ui'; -const pageSizeOptions = [5, 10, 20]; +const pageSizeOptions = [9, 18, 36]; + interface FooterProps { totalRows: number; @@ -53,7 +54,7 @@ const Footer: React.FC = ({ }; return ( -
+
-
-
- - - -
-
- - - - )} - - - - Go back to Drafts{' '} - - - - - - - ); -}; - -const Navigation = ({ - id, - pathItem, - organization, -}: { - id: string; - pathItem: string; - organization: string; -}) => { - let links = [ - { - label: 'Resources', - url: `/dashboard/organization/${organization}/dataset/${id}/edit/resources`, - selected: pathItem === 'resources', - }, - { - label: 'Access Models', - url: `/dashboard/organization/${organization}/dataset/${id}/edit/access?list=true`, - selected: pathItem === 'access', - }, - { - label: 'Metadata', - url: `/dashboard/organization/${organization}/dataset/${id}/edit/metadata`, - selected: pathItem === 'metadata', - }, - { - label: 'Publish', - url: `/dashboard/organization/${organization}/dataset/${id}/edit/publish`, - selected: pathItem === 'publish', - }, - ]; - - const router = useRouter(); - - const handleTabClick = (url: string) => { - router.replace(url); - }; - - const initialTabLabel = - links.find((option) => option.selected)?.label || 'Distributions'; - - return ( -
- - - {links.map((item, index) => ( - handleTabClick(item.url)} - > - {item.label} - - ))} - - -
- ); -}; diff --git a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditMetadata.tsx b/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditMetadata.tsx deleted file mode 100644 index 02bf6cfd..00000000 --- a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditMetadata.tsx +++ /dev/null @@ -1,362 +0,0 @@ -'use client'; - -import { useParams, useRouter } from 'next/navigation'; -import { graphql } from '@/gql'; -import { - TypeCategory, - TypeDataset, - TypeDatasetMetadata, - TypeMetadata, - TypeTag, - UpdateMetadataInput, -} from '@/gql/generated/graphql'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { - Button, - Combobox, - Divider, - Form, - FormLayout, - Input, - Text, - toast, -} from 'opub-ui'; - -import { GraphQL } from '@/lib/api'; -import { Loading } from '@/components/loading'; - -const categoriesListQueryDoc: any = graphql(` - query CategoryList { - categories { - id - name - } - } -`); - -const tagsListQueryDoc: any = graphql(` - query TagsList { - tags { - id - value - } - } -`); - -const datasetMetadataQueryDoc: any = graphql(` - query MetadataValues($filters: DatasetFilter) { - datasets(filters: $filters) { - title - id - description - tags { - id - value - } - categories { - id - name - } - metadata { - metadataItem { - id - label - } - id - value - } - } - } -`); - -const metadataQueryDoc: any = graphql(` - query MetaDataList($filters: MetadataFilter) { - metadata(filters: $filters) { - id - label - dataStandard - urn - dataType - options - validator - type - model - enabled - filterable - } - } -`); - -const updateMetadataMutationDoc: any = graphql(` - mutation SaveMetadata($UpdateMetadataInput: UpdateMetadataInput!) { - addUpdateDatasetMetadata(updateMetadataInput: $UpdateMetadataInput) { - __typename - ... on TypeDataset { - id - created - } - ... on OperationInfo { - messages { - kind - message - } - } - } - } -`); - -export function EditMetadata({ id }: { id: string }) { - const router = useRouter(); - const params = useParams(); - - const queryClient = useQueryClient(); - - const getCategoriesList: { data: any; isLoading: boolean; error: any } = - useQuery([`categories_list_query`], () => - GraphQL(categoriesListQueryDoc, []) - ); - - const getTagsList: { data: any; isLoading: boolean; error: any } = useQuery( - [`tags_list_query`], - () => GraphQL(tagsListQueryDoc, []) - ); - - const getMetaDataListQuery: { - data: any; - isLoading: boolean; - refetch: any; - } = useQuery([`metadata_fields_list_${id}`], () => - GraphQL(metadataQueryDoc, { - filters: { - model: 'DATASET', - enabled: true, - }, - }) - ); - - const getDatasetMetadata: { - data: any; - isLoading: boolean; - refetch: any; - error: any; - } = useQuery([`metadata_values_query_${id}`], () => - GraphQL(datasetMetadataQueryDoc, { filters: { id: id } }) - ); - - const updateMetadataMutation = useMutation( - (data: { UpdateMetadataInput: UpdateMetadataInput }) => - GraphQL(updateMetadataMutationDoc, data), - { - onSuccess: (data: any) => { - toast('Details updated successfully!'); - - queryClient.invalidateQueries({ - queryKey: [ - `metadata_values_query_${id}`, - `metadata_fields_list_${id}`, - ], - }); - - getMetaDataListQuery.refetch(); - getDatasetMetadata.refetch(); - - router.push( - `/dashboard/organization/${params.organizationId}/dataset/${id}/edit/publish` - ); - }, - onError: (err: any) => { - toast('Error: ' + err.message.split(':')[0]); - }, - } - ); - - const defaultValuesPrepFn = (dataset: TypeDataset) => { - // Function to set default values for the form - - let defaultVal: { - [key: string]: any; - } = {}; - - dataset.metadata?.map((field) => { - defaultVal[field.metadataItem.id] = field.value; - }); - - defaultVal['description'] = dataset.description || ''; - - defaultVal['categories'] = - dataset.categories?.map((category: TypeCategory) => { - return { - label: category.name, - value: category.id, - }; - }) || []; - - defaultVal['tags'] = - dataset.tags?.map((tag: TypeTag) => { - return { - label: tag.value, - value: tag.id, - }; - }) || []; - - return defaultVal; - }; - - return ( - <> -
{ - // Call the mutation to save both the static and dynamic metadata - updateMetadataMutation.mutate({ - UpdateMetadataInput: { - dataset: id, - metadata: [ - ...Object.keys(values) - .filter( - (valueItem) => - !['categories', 'description', 'tags'].includes(valueItem) - ) - .map((key) => { - return { - id: key, - value: values[key] || '', - }; - }), - ], - description: values.description || '', - tags: values.tags?.map((item: any) => item.label) || [], - categories: - values.categories?.map((item: any) => item.value) || [], - }, - }); - }} - formOptions={{ - resetOptions: { - keepValues: true, - keepDirtyValues: true, - }, - defaultValues: defaultValuesPrepFn( - getDatasetMetadata.isLoading || getDatasetMetadata.error - ? {} - : getDatasetMetadata?.data?.datasets[0] - ), - }} - > - <> -
- -
- -
- -
-
- { - return { - label: item.value, - value: item.id, - }; - }) || [] - } - label="Tags" - /> -
-
- { - return { label: item.name, value: item.id }; - } - ) || [] - } - name="categories" - /> -
-
- - {getMetaDataListQuery.isLoading ? ( - - ) : getMetaDataListQuery?.data?.metadata?.length > 0 ? ( - <> -
- -
- -
- Add Metadata -
- -
- -
- -
- {getMetaDataListQuery?.data?.metadata?.map( - (metadataFormItem: TypeMetadata) => { - if (metadataFormItem.dataType === 'STRING') { - return ( -
- -
- ); - } - return null; - } - )} -
- - ) : ( - <> - )} -
-
-
- -
-
- - -
- -
- - ); -} diff --git a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditResource.tsx b/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditResource.tsx deleted file mode 100644 index 61023018..00000000 --- a/app/[locale]/dashboard/organization/[organizationId]/dataset/[id]/edit/components/EditResource.tsx +++ /dev/null @@ -1,496 +0,0 @@ -import React from 'react'; -import { useParams } from 'next/navigation'; -import { graphql } from '@/gql'; -import { - CreateFileResourceInput, - SchemaUpdateInput, - UpdateFileResourceInput, -} from '@/gql/generated/graphql'; -import { IconTrash } from '@tabler/icons-react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { - parseAsBoolean, - parseAsString, - useQueryState, -} from 'next-usequerystate'; -import { - Button, - ButtonGroup, - Checkbox, - Combobox, - DataTable, - Dialog, - Divider, - DropZone, - Icon, - IconButton, - Select, - Spinner, - Text, - TextField, - toast, -} from 'opub-ui'; - -import { GraphQL } from '@/lib/api'; -import { Icons } from '@/components/icons'; -import { createResourceFilesDoc } from './ResourceDropzone'; -import { ResourceSchema, updateSchema } from './ResourceSchema'; - -interface TListItem { - label: string; - value: string; - description: string; - dataset: any; - fileDetails: any; -} - -export const EditResource = ({ reload, data }: any) => { - const params = useParams(); - - const updateResourceDoc: any = graphql(` - mutation updateFileResource($fileResourceInput: UpdateFileResourceInput!) { - updateFileResource(fileResourceInput: $fileResourceInput) { - __typename - ... on TypeResource { - id - description - name - } - } - } - `); - - const [resourceId, setResourceId] = useQueryState('id', parseAsString); - const [schema, setSchema] = React.useState([]); - - const { mutate, isLoading } = useMutation( - (data: { - fileResourceInput: UpdateFileResourceInput; - isResetSchema: boolean; - }) => GraphQL(updateResourceDoc, data), - { - onSuccess: (data, variables) => { - toast('File changes saved', { - action: { - label: 'Dismiss', - onClick: () => {}, - }, - }); - if (variables.isResetSchema) { - schemaMutation.mutate({ - resourceId: resourceId, - }); - } - reload(); - }, - onError: (err: any) => { - console.log('Error ::: ', err); - }, - } - ); - - const resetSchema: any = graphql(` - mutation resetFileResourceSchema($resourceId: UUID!) { - resetFileResourceSchema(resourceId: $resourceId) { - ... on TypeResource { - id - schema { - format - description - id - fieldName - } - } - } - } - `); - - const schemaMutation = useMutation( - (data: { resourceId: string }) => GraphQL(resetSchema, data), - { - onSuccess: () => { - schemaQuery.refetch(); - }, - onError: (err: any) => { - console.log('Error ::: ', err); - }, - } - ); - - const schemaQuery = useQuery([`fetch_schema_${params.id}`], () => - GraphQL(fetchSchema, { datasetId: params.id }) - ); - - const fetchSchema: any = graphql(` - query datasetSchema($datasetId: UUID!) { - datasetResources(datasetId: $datasetId) { - schema { - id - fieldName - format - description - } - id - } - } - `); - - const { mutate: modify } = useMutation( - (data: { input: SchemaUpdateInput }) => GraphQL(updateSchema, data), - { - onSuccess: () => { - schemaQuery.refetch(); - toast('Schema Updated Successfully', { - action: { - label: 'Dismiss', - onClick: () => {}, - }, - }); - }, - onError: (err: any) => { - console.log('Error ::: ', err); - }, - } - ); - - const { mutate: transform } = useMutation( - (data: { fileResourceInput: CreateFileResourceInput }) => - GraphQL(createResourceFilesDoc, data), - { - onSuccess: (data: any) => { - setResourceId(data.createFileResources[0].id); - toast('Resource Added Successfully', { - action: { - label: 'Dismiss', - onClick: () => {}, - }, - }); - schemaQuery.refetch(); - reload(); - }, - onError: (err: any) => { - console.log('Error ::: ', err); - }, - } - ); - - const ResourceList: TListItem[] = - data.map((item: any) => ({ - label: item.name, - value: item.id, - description: item.description, - dataset: item.dataset?.pk, - fileDetails: item.fileDetails, - })) || []; - - const getResourceObject = (resourceId: string) => { - return ResourceList.find((item) => item.value === resourceId); - }; - - const [resourceName, setResourceName] = React.useState( - getResourceObject(resourceId)?.label - ); - const [resourceDesc, setResourceDesc] = React.useState( - getResourceObject(resourceId)?.description - ); - - const handleNameChange = (text: string) => { - setResourceName(text); - }; - const handleDescChange = (text: string) => { - setResourceDesc(text); - }; - - React.useEffect(() => { - setResourceName(getResourceObject(resourceId)?.label); - setResourceDesc(getResourceObject(resourceId)?.description); - - //fix this later - }, [JSON.stringify(ResourceList), resourceId]); - - const handleResourceChange = (e: any) => { - setResourceId(e, { shallow: false }); - setResourceName(getResourceObject(e)?.label); - setResourceDesc(getResourceObject(e)?.description); - }; - - const [file, setFile] = React.useState([]); - - const dropZone = React.useCallback( - (_dropFiles: File[], acceptedFiles: File[]) => { - transform({ - fileResourceInput: { - dataset: params.id, - files: acceptedFiles, - }, - }); - setFile((files) => [...files, ...acceptedFiles]); - }, - [] - ); - - const uploadedFile = file.length > 0 && ( -
- {file.map((file, index) => { - return
{file.name}
; - })} -
- ); - - - const onDrop = React.useCallback( - (_dropFiles: File[], acceptedFiles: File[]) => { - mutate({ - fileResourceInput: { - id: resourceId, - file: acceptedFiles[0], - }, - isResetSchema: true, - }); - }, - [] - ); - const fileInput = ( -
- - {getResourceObject(resourceId)?.fileDetails.file.name.replace( - 'resources/', - '' - )}{' '} - -
- ); - - const listViewFunction = () => { - setResourceId(''); - }; - - const saveResource = () => { - mutate({ - fileResourceInput: { - id: resourceId, - description: resourceDesc || '', - name: resourceName || '', - }, - isResetSchema: false, - }); - if (schema.length > 0) { - const updatedScheme = schema.map((item) => { - const { fieldName, ...rest } = item as any; - return rest; - }); - modify({ - input: { - resource: resourceId, - updates: updatedScheme, - }, - }); - } - }; - - return ( -
-
- Resource Name : -
-