Skip to content
This repository was archived by the owner on Jul 20, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ on:
- 'develop'

env:
APP_TITLE: ${{ vars.APP_TITLE }}
APP_GRAPHQL_CODEGEN_ENDPOINT: ${{ vars.APP_GRAPHQL_CODEGEN_ENDPOINT }}
APP_GRAPHQL_ENDPOINT: ${{ vars.APP_GRAPHQL_ENDPOINT }}
APP_TITLE: CAPN CMS
APP_GRAPHQL_CODEGEN_ENDPOINT: ./backend/schema.graphql
APP_GRAPHQL_ENDPOINT: http://localhost:8000/graphql/
GITHUB_WORKFLOW: true

jobs:
test:
name: Run tests
environment: 'test'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -36,7 +35,6 @@ jobs:
run: pnpm test
unused:
name: Find unused things
environment: 'test'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -59,7 +57,6 @@ jobs:
run: pnpm lint:unused
lint-js:
name: Lint JS
environment: 'test'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -82,7 +79,6 @@ jobs:
run: pnpm lint:js
css-lint:
name: Lint CSS
environment: 'test'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -103,7 +99,6 @@ jobs:
run: pnpm lint:css
typecheck:
name: Typecheck
environment: 'test'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -126,7 +121,6 @@ jobs:
run: pnpm typecheck
build:
name: Build
environment: 'test'
needs: [lint-js, css-lint, typecheck]
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion backend
Submodule backend updated 79 files
+3 −3 .github/workflows/ci.yml
+34 −7 .github/workflows/helm-publish.yml
+3 −2 apps/blog/graphql/filters.py
+4 −4 apps/blog/graphql/inputs.py
+3 −4 apps/blog/graphql/mutations.py
+1 −1 apps/blog/graphql/types.py
+3 −3 apps/blog/migrations/0002_blog_status.py
+18 −0 apps/blog/migrations/0003_blog_slug.py
+29 −0 apps/blog/migrations/0004_alter_author_image_alter_blog_cover_image_and_more.py
+14 −11 apps/blog/models.py
+16 −13 apps/blog/serializers.py
+1 −9 apps/blog/tests/mutations_test.py
+21 −6 apps/common/admin.py
+27 −1 apps/common/factories.py
+14 −11 apps/common/graphql/filters.py
+41 −6 apps/common/graphql/inputs.py
+94 −35 apps/common/graphql/mutations.py
+12 −1 apps/common/graphql/orders.py
+31 −12 apps/common/graphql/queries.py
+29 −4 apps/common/graphql/types.py
+72 −3 apps/common/migrations/0001_initial.py
+0 −38 apps/common/migrations/0002_alter_report_status_galleryimage.py
+19 −0 apps/common/migrations/0002_report_cover_image.py
+44 −0 apps/common/migrations/0003_alter_artwork_image_alter_eventasset_image_and_more.py
+0 −54 apps/common/migrations/0003_galleryitem_youtubevideo_delete_galleryimage.py
+0 −18 apps/common/migrations/0004_alter_youtubevideo_release_date.py
+29 −13 apps/common/models.py
+45 −31 apps/common/serializers.py
+410 −74 apps/common/tests/mutations_test.py
+205 −18 apps/common/tests/queries_test.py
+2 −2 apps/news/graphql/filters.py
+1 −2 apps/news/graphql/queries.py
+8 −2 apps/news/graphql/types.py
+2 −2 apps/news/migrations/0001_initial.py
+18 −0 apps/news/migrations/0002_news_slug.py
+7 −2 apps/news/models.py
+1 −1 apps/news/tests/mutations_test.py
+1 −7 apps/news/tests/queries_test.py
+4 −3 apps/podcast/graphql/filters.py
+5 −4 apps/podcast/graphql/inputs.py
+49 −25 apps/podcast/graphql/mutations.py
+8 −9 apps/podcast/graphql/queries.py
+2 −2 apps/podcast/graphql/types.py
+23 −0 apps/podcast/migrations/0002_podcastseason_is_archived_voxpopseason_is_archived.py
+24 −0 apps/podcast/migrations/0003_alter_podcastepisode_thumbnail_and_more.py
+5 −2 apps/podcast/models.py
+4 −42 apps/podcast/serializers.py
+103 −14 apps/podcast/tests/mutations_test.py
+6 −26 apps/podcast/tests/queries_test.py
+1 −1 apps/team/graphql/filters.py
+1 −1 apps/team/graphql/inputs.py
+2 −3 apps/team/graphql/queries.py
+7 −2 apps/team/graphql/types.py
+24 −0 apps/team/migrations/0002_teammember_bio_alter_teammember_member_photo.py
+3 −1 apps/team/models.py
+0 −3 apps/team/tests/mutations_test.py
+0 −11 apps/team/tests/queries_test.py
+1 −2 apps/user/graphql/queries.py
+0 −5 apps/user/tests/query_test.py
+3 −3 apps/vacancy/graphql/filters.py
+4 −5 apps/vacancy/graphql/queries.py
+16 −11 apps/vacancy/graphql/types.py
+5 −5 apps/vacancy/tests/mutations_test.py
+3 −14 apps/vacancy/tests/queries_test.py
+3 −27 docker-compose.yaml
+3 −3 helm/Chart.lock
+1 −1 helm/Chart.yaml
+77 −0 helm/linter_values.yaml
+0 −45 helm/values-test.yaml
+95 −28 helm/values.yaml
+0 −1 main/sentry.py
+12 −29 main/settings.py
+4 −2 main/urls.py
+1 −1 pyproject.toml
+231 −128 schema.graphql
+15 −0 utils/common.py
+19 −0 utils/fields.py
+32 −0 utils/graphql/types.py
+56 −2 uv.lock
2 changes: 1 addition & 1 deletion src/App/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ const events = customWrapRoute({
visibility: 'is-authenticated',
},
});

/*
const userManagement = customWrapRoute({
parent: homeLayout,
Expand Down Expand Up @@ -301,6 +300,7 @@ const wrappedRoutes = {
reports,
podcast,
youtubeVideos,

// userActivation,
// editProfile,
// forgotPassword,
Expand Down
1 change: 1 addition & 0 deletions src/components/Chip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const chipVariantToClassNameMap: Record<ChipVariant, string> = {
variant?: ChipVariant;
}

/** @knipignore */
function Chip(props: Props) {
const {
className,
Expand Down
209 changes: 209 additions & 0 deletions src/components/FileInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import {
useCallback,
useEffect,
useState,
} from 'react';
import { IoTrash } from 'react-icons/io5';
import { _cs } from '@togglecorp/fujs';
import {
Button,
InputContainer,
type InputContainerProps,
useButtonFeatures,
} from '@togglecorp/toggle-ui';

import RawFileInput, { RawFileInputProps } from '#components/RawFileInput';
import useDropHandler from '#hooks/useDropHandler';
import isValidFile, { ErrorType } from '#utils/common';

import styles from './styles.module.css';

type NameType = string | number | undefined;

type InheritedProps<T extends NameType> = (
Omit<InputContainerProps, 'input'> &
Omit<RawFileInputProps<T>, 'onChange' | 'value' | 'id'>
);

/** @knipignore */
// eslint-disable-next-line import/prefer-default-export
export type Props<T extends NameType> = InheritedProps<T> & {
inputElementRef?: React.RefObject<HTMLInputElement>;
inputClassName?: string;
labelClassName?: string;
value: File | undefined | null;
onChange: (file: File | null, name: T) => void;
status?: string;
showFileName?: boolean;
placeholder?: string;
maxFileSize?: number;
};

function FileInput<T extends NameType>(props: Props<T>) {
const {
className,
disabled,
error,
errorContainerClassName,
inputSectionClassName,
inputContainerClassName,
label,
readOnly,
inputElementRef,
containerRef,
inputSectionRef,
inputClassName,
value,
onChange,
name: nameFromProps,
accept,
labelClassName,
children,
showFileName = true,
maxFileSize = 10,
...fileInputProps
} = props;

const [internalError, setInternalError] = useState<string>();
const [fileName, setFileName] = useState<string | undefined>(value?.name);

useEffect(() => {
setFileName(value?.name);
}, [value]);

const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null;

if (file) {
const validity = isValidFile(file, maxFileSize, accept);
if (!validity.isValid) {
if (validity.errorType === ErrorType.invalidFileType) {
setInternalError('Invalid file type.');
} else {
setInternalError(`File exceeds size limit of ${maxFileSize} MB.`);
}
onChange(null, nameFromProps);
return;
}
}
setInternalError(undefined);
setFileName(file?.name);
onChange(file, nameFromProps);
}, [accept, maxFileSize, nameFromProps, onChange]);

const handleDrop: React.DragEventHandler<HTMLDivElement> = useCallback((e) => {
e.preventDefault();
const file = e.dataTransfer.files?.[0] ?? null;

if (file) {
const validity = isValidFile(file, maxFileSize, accept);
if (!validity.isValid) {
if (validity.errorType === ErrorType.invalidFileType) {
setInternalError('Invalid file type.');
} else {
setInternalError(`File exceeds size limit of ${maxFileSize} MB.`);
}
onChange(null, nameFromProps);
return;
}
}

setInternalError(undefined);
setFileName(file?.name);
onChange(file, nameFromProps);
e.dataTransfer.clearData();
}, [accept, maxFileSize, nameFromProps, onChange]);

const {
dropping,
onDragOver,
onDragEnter,
onDragLeave,
onDrop,
} = useDropHandler(handleDrop);

const inputId = `file-input-${String(nameFromProps)}`;

const {
className: buttonLabelClassName,
children: buttonLabelChildren,
} = useButtonFeatures({
className: labelClassName,
disabled,
variant: 'primary',
children: (
<>
{children}
<RawFileInput
id={inputId}
className={styles.input}
inputRef={inputElementRef}
readOnly={readOnly}
disabled={disabled}
name={nameFromProps}
accept={accept}
onChange={handleFileChange}
// eslint-disable-next-line react/jsx-props-no-spreading
{...fileInputProps}
/>
</>
),
});

return (
<InputContainer
className={_cs(styles.inputContainer, className)}
containerRef={containerRef}
inputContainerClassName={_cs(inputContainerClassName)}
inputSectionClassName={_cs(styles.inputSection, inputSectionClassName)}
errorContainerClassName={errorContainerClassName}
inputSectionRef={inputSectionRef}
disabled={disabled}
error={error ?? internalError}
label={label}
readOnly={readOnly}
actions={(
<Button
name="clear"
type="button"
className={styles.clearButton}
onClick={() => {
setInternalError(undefined);
setFileName(undefined);
onChange(null, nameFromProps);
}}
disabled={disabled}
transparent
>
<IoTrash />
</Button>
)}
input={(
<div
className={_cs(
inputClassName,
styles.inputChildren,
dropping && styles.draggedOver,
)}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
>
<div className={styles.browseButton}>
<label className={buttonLabelClassName} htmlFor={inputId}>
{buttonLabelChildren}
</label>
</div>
{showFileName && fileName && (
<div className={styles.fileName}>
{fileName}
</div>
)}
</div>
)}
/>
);
}

export default FileInput;
15 changes: 15 additions & 0 deletions src/components/FileInput/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.input-container {
.input-section {
background-color: var(--cms-ui-color-gray-20);

.input-children {
display: flex;
align-items: start;
gap: var(--cms-ui-spacing-lg);
}

.input {
visibility: hidden;
}
}
}
52 changes: 52 additions & 0 deletions src/components/RawFileInput/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { _cs } from '@togglecorp/fujs';

import styles from './styles.module.css';

export type RawFileInputProps<NAME extends number | string | undefined> = {
id: string;
accept?: string;
disabled?: boolean;
inputProps?: React.ComponentPropsWithoutRef<'input'>;
inputRef?: React.RefObject<HTMLInputElement>;
name: NAME;
readOnly?: boolean;
className?: string;
children?: React.ReactNode;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

function RawFileInput<NAME extends number | string | undefined>(props: RawFileInputProps<NAME>) {
const {
id,
accept,
disabled,
inputProps,
inputRef,
name,
onChange,
readOnly,
className,
children,
} = props;

return (
<label htmlFor={id} className={_cs(styles.fileInput, className)}>
{children}
<input
id={id}
className={styles.input}
type="file"
accept={accept}
onChange={onChange}
name={name !== undefined ? String(name) : undefined}
ref={inputRef}
disabled={disabled}
readOnly={readOnly}
// eslint-disable-next-line react/jsx-props-no-spreading
{...inputProps}
/>
</label>
);
}

export default RawFileInput;
7 changes: 7 additions & 0 deletions src/components/RawFileInput/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.file-input {
.input {
visibility: hidden;
width: 0;
height: 0;
}
}
Loading