Compare commits

...

58 Commits

Author SHA1 Message Date
Gimanh b24ab9b352 Merge pull request #24 from Gimanh/chore/version-1-20-4
chore: version
2026-02-22 21:18:45 +01:00
Nikolai Giman b0482d438f chore: version 2026-02-22 21:18:20 +01:00
Gimanh 8298125080 Merge pull request #23 from Gimanh/fix/mobile-build
fix: mobile build
2026-02-22 21:08:16 +01:00
Nikolai Giman 47492637b4 fix: mobile build 2026-02-22 21:05:29 +01:00
Gimanh 1c25d1a1bd Merge pull request #22 from Gimanh/feat/version
chore: version
2026-02-22 20:33:38 +01:00
Nikolai Giman f24536313c chore: version 2026-02-22 20:33:16 +01:00
Gimanh 2a56c7c666 Merge pull request #21 from Gimanh/fix/save-locale
Fix/save locale
2026-02-22 20:28:04 +01:00
Nikolai Giman fd39c5303a fix: save the locale in storage 2026-02-22 19:52:39 +01:00
Nikolai Giman 123733e44d fix: types 2026-02-22 19:41:40 +01:00
Gimanh 38d4e39818 Merge pull request #20 from Gimanh/fix/urls
fix: urls
2026-02-22 15:39:41 +01:00
Nikolai Giman 70a321b418 fix: urls 2026-02-22 15:38:58 +01:00
Gimanh ed248c42cd Merge pull request #19 from Gimanh/fix/external-auth
fix: external auth and up version
2026-02-22 14:51:55 +01:00
Nikolai Giman 40f24c7933 fix: external auth and up version 2026-02-22 14:51:31 +01:00
Gimanh ef4601d4a9 Merge pull request #18 from Gimanh/feat/nuxt-ui-client
feat: web and mobile client in with nuxt-ui lib
2026-02-21 22:16:40 +01:00
Nikolai Giman 24341951cd fix: update example env and version 2026-02-21 21:20:26 +01:00
Nikolai Giman b6ba0fbf9b fix: renamed directory web-nuxt-ui => web 2026-02-21 18:48:38 +01:00
Nikolai Giman 1064b88902 fix: add docker scripts 2026-02-21 18:36:22 +01:00
Nikolai Giman b2eab9ec61 fix: renamed old folder 2026-02-21 18:26:35 +01:00
Nikolai Giman 3d0d3eca47 feat: web and mobile client in with nuxt-ui lib 2026-02-21 18:16:12 +01:00
Gimanh 55e5ae4335 Merge pull request #17 from Gimanh/fix/start-screen-loading
fix: start screen loading
2026-02-21 13:06:52 +01:00
Nikolai Giman 3c456edbd8 fix: validate tag and task belong to the same project 2026-02-21 13:00:58 +01:00
Nikolai Giman dc25a02ff2 fix: start screen loading 2026-02-16 00:31:22 +01:00
Gimanh 5c200c2704 Merge pull request #16 from Gimanh/fix/some-fixes
Fix/some fixes
2026-02-14 19:05:43 +01:00
Nikolai Giman 5149e7101e fix: types 2026-02-14 19:03:09 +01:00
Nikolai Giman 471a82b9f5 fix: fix issues 2026-02-08 22:06:49 +01:00
Gimanh 072ad5d629 Merge pull request #15 from Gimanh/chore/ios-version
chore: up ios version
2026-01-25 21:41:59 +01:00
Nikolai Giman dc5fd90bca chore: up ios version 2026-01-25 21:41:30 +01:00
Gimanh 55f6347784 Merge pull request #14 from Gimanh/feat/apple-login
feat: login by Apple
2026-01-25 19:20:44 +01:00
Nikolai Giman 165c1b2444 feat: login by Apple 2026-01-25 19:19:54 +01:00
Gimanh b3cc983965 Merge pull request #13 from Gimanh/chore/version
chore: version
2026-01-23 21:58:10 +01:00
Nikolai Giman a14c332586 chore: version 2026-01-23 21:57:29 +01:00
Gimanh aa91934f19 Merge pull request #12 from Gimanh/fix/use-cookie
fix: use cookie
2026-01-22 20:03:13 +01:00
Gimanh aa8e6e2da6 Merge branch 'main' into fix/use-cookie 2026-01-22 20:02:40 +01:00
Nikolai Giman c4c9a16a09 fix: use cookie 2026-01-22 20:01:26 +01:00
Gimanh 293340c253 Merge pull request #11 from Gimanh/chore/docker-build
add bash script
2026-01-19 00:19:03 +01:00
Nikolai Giman 04bb545c25 add bash script 2026-01-19 00:17:56 +01:00
Gimanh aa235c10ea Merge pull request #10 from Gimanh/chore/extend-app
chore: add extend methods
2026-01-18 23:57:14 +01:00
Nikolai Giman 45e4a11605 chore: add extend methods 2026-01-18 23:56:23 +01:00
Gimanh 55b42dfecb Merge pull request #9 from Gimanh/chore/deps
chore: deps
2026-01-18 22:28:34 +01:00
Nikolai Giman a620c39b50 chore: deps 2026-01-18 22:24:59 +01:00
Nikolai Giman 8edb2d50d3 chore: deps 2026-01-18 22:17:40 +01:00
Nikolai Giman 19093598a0 chore: deps 2026-01-18 22:13:59 +01:00
Gimanh 9091d576e1 Merge pull request #8 from Gimanh/feat/google-auth
feat: add login by Google and GitHub
2026-01-12 21:36:24 +01:00
Nikolai Giman 7d9e4a811a feat: update app version 2026-01-12 00:40:18 +01:00
Nikolai Giman 1b03e61d2b feat: add login by Google and GitHub 2026-01-11 23:22:30 +01:00
Gimanh 0936f51a24 Merge pull request #7 from Gimanh/fix/history-deletion
fix: delete history cascade with entity (goal, list, tasks)
2026-01-08 22:10:00 +01:00
Nikolai Giman c4c6d91456 fix: delete history cascade with entity (goal, list, tasks) 2026-01-08 22:08:23 +01:00
Gimanh 1ba13eef45 Merge pull request #5 from Gimanh/fix/load-more-in-kanban
fix: increase task limit in TasksRepository and handle columnId in Ka…
2026-01-05 20:01:40 +01:00
Nikolai Giman 8a67296673 fix: increase task limit in TasksRepository and handle columnId in KanbanBoard to fix load more issue 2026-01-05 20:00:18 +01:00
Gimanh 8fe3353c5f Merge pull request #4 from Gimanh/fix/subtask-add-ui-bug
fix: add subtasks to selectedTask
2026-01-04 23:58:58 +01:00
Nikolai Giman 61debcda11 fix: add subtasks to selectedTask 2026-01-04 23:57:36 +01:00
Gimanh 615a47eafd Merge pull request #3 from Gimanh/chore/build
chore: update build scripts and package configurations
2026-01-04 22:04:12 +01:00
Nikolai Giman 4705055062 chore: update node to version 24 2026-01-04 22:03:03 +01:00
Nikolai Giman 7426f72257 chore: update build scripts and package configurations 2026-01-04 16:54:08 +01:00
Gimanh 64ddf4acfa Merge pull request #2 from Gimanh/chore/license-file-name
chore: update md
2026-01-03 20:57:30 +01:00
Nikolai Giman d843008091 chore: update md 2026-01-03 20:56:44 +01:00
Gimanh 95cbd330b1 Merge pull request #1 from Gimanh/chore/license-file-name
chore: license & cla
2026-01-03 17:34:07 +01:00
Nikolai Giman cf7055ce60 chore: license & cla 2026-01-03 17:24:57 +01:00
732 changed files with 27246 additions and 8882 deletions
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_CLA }}
with:
path-to-signatures: 'signatures/cla.json'
path-to-document: 'https://github.com/Gimanh/test-cla1/blob/main/CLA/CLA.md'
path-to-document: 'https://github.com/Gimanh/taskview-community/blob/main/CLA/CLA.md'
branch: 'cla-signatures'
allowlist: bot*,dependabot[bot]
custom-notsigned-prcomment: >
Thank you for your contribution!
Please read the CLA: $pathToCLADocument
Please read the CLA: https://github.com/Gimanh/taskview-community/blob/main/CLA/CLA.md
Then comment exactly:
"I have read the CLA Document and I hereby sign the CLA"
+5 -1
View File
@@ -35,4 +35,8 @@ tsconfig.app.tsbuildinfo
customer-license
!build-dockers.sh
!build-docker-api.sh
!build-docker-web.sh
!build-docker-web.sh
!entrypoint.sh
!build-docker-migrations.sh
*.private
*.private*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+7 -2
View File
@@ -1,6 +1,6 @@
# TaskView Source-Available License
Version 1.0 Draft (tailored for TaskView)
Version 1.0
---
@@ -113,9 +113,14 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMA
For commercial licensing inquiries, contact:
**[licensing@taskview.tech](mailto:licensing@taskview.tech)**
**[support@taskview.tech](mailto:support@taskview.tech)**
## 10. Termination
- Any violation of Sections 4 or 5 automatically terminates the rights granted under this License.
- Rights may be reinstated if You cure the violation within 21 days and cease all prohibited use.
---
Copyright (c) 2026 Nikolai Giman
All rights reserved.
+6 -2
View File
@@ -127,7 +127,7 @@ docker-compose up
Make sure the image versions match the version defined in the root package.json.
## Roadmap
- Migrate api-server to BunJS
- Plugin / extension system
- Migrate to NuxtUI or similar ui library
- Enterprise SSO and identity integrations
@@ -137,4 +137,8 @@ Make sure the image versions match the version defined in the root package.json.
Note for contributors: contributions are accepted under the CLA (see CONTRIBUTING.md). The Project is distributed under the TaskView Source-Available License.
© TaskView
---
TaskView is developed and maintained by Nikolai Giman.
Copyright © 2026 Nikolai Giman
+15 -10
View File
@@ -5,14 +5,16 @@
VERSION=$1
if [ -z "$VERSION" ]; then
echo "Usage: ./build-docker-api.sh <version>"
echo "Example: ./build-docker-api.sh 1.17.0"
exit 1
fi
node -v
# Build the server scripts for the docker container (obfuscated scripts)
npm run vite-build-docker
# Create the taskview-server.js file for running the server
# (taskview-server-api.jsc is bytecode file that will be created by bytenode when docker file is run)
echo "const bytenode = require('bytenode'); require('./taskview-server-api.jsc');" > ./dist/taskview-server.js
# Build the server scripts for the docker container
npm run build:docker
# Copy the production package.json to the dist folder
cp ./production.package.json ./dist/package.json
@@ -20,14 +22,17 @@ cp ./production.package.json ./dist/package.json
# Copy the production ecosystem.config.js to the dist folder
cp ./production.ecosystem.config.js ./dist/ecosystem.config.js
# Build the migration scripts for the docker container (obfuscated scripts)
npm run vite-build-migration
mkdir ./dist-migration/taskview
# Build the migration scripts for the docker container
npm run build:migration
mkdir -p ./dist-migration/taskview
cp -R ./src/migrations/taskview/* ./dist-migration/taskview
node ./commands/copy-migration-files.js
echo "Building docker image..."
docker buildx build --platform=linux/amd64,linux/arm64 -t gimanhead/taskview-ce-api-server:$VERSION -t gimanhead/taskview-ce-api-server:latest . --load
cd postgresql
bash ./build-docker-migrations.sh $VERSION
cd ..
cd ..
echo "Build complete! Image: gimanhead/taskview-ce-api-server:$VERSION"
+1
View File
@@ -16,6 +16,7 @@ SMTP_PASSWORD="smtp-password"
SMTP_ENCRYPTION="ssl"
SMTP_FROM_NAME="TaskViewApiServer"
SMTP_FROM_EMAIL="smtp"
CORS_ALLOWED_ORIGINS="http://localhost:5173,http://127.0.0.1:5173,http://localhost:3000,http://localhost:8888,http://127.0.0.1:3000,http://127.0.0.1:8888"
# Constant value
APP_URL="https://taskview.handscream.com"
+3 -3
View File
@@ -14,7 +14,7 @@ services:
timeout: 5s
retries: 5
migration:
image: gimanhead/taskview-ce-db-migration:1.18.0
image: gimanhead/taskview-ce-db-migration:latest
restart: "no"
depends_on:
db:
@@ -23,12 +23,12 @@ services:
- ./.env.taskview
taskview-ce-webapp:
image: gimanhead/taskview-ce-webapp:1.18.0
image: gimanhead/taskview-ce-webapp:latest
restart: "no"
ports:
- "8888:80"
taskview-api-server:
image: gimanhead/taskview-ce-api-server:1.18.0
image: gimanhead/taskview-ce-api-server:latest
restart: "no"
ports:
- "1725:1401"
+3 -10
View File
@@ -1,10 +1,6 @@
FROM node:20-bullseye-slim
FROM node:24.12.0-alpine
RUN apt-get update && apt-get install -y \
curl \
build-essential \
qemu-user-static \
&& rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl
RUN npm install pm2 -g
@@ -12,10 +8,7 @@ WORKDIR /usr/src/app
COPY ./dist .
RUN npm install
RUN npm run byte
RUN rm ./taskview-server-api.js
RUN rm ./taskview-server-docker.js
RUN npm install --omit=dev
EXPOSE 1401
+21 -14
View File
@@ -1,15 +1,13 @@
{
"name": "taskview-ce-api-server",
"version": "1.17.0",
"version": "1.20.4",
"scripts": {
"byte": "bytenode --compile ./dist/taskview-server-api.js",
"vite-build": "vite build && javascript-obfuscator ./dist/taskview-server.js --output ./dist/taskview-server-api.js && npm run byte",
"vite-build-docker": "vite build --config ./vite.config.docker.mts && javascript-obfuscator ./dist/taskview-server-docker.js --output ./dist/taskview-server-api.js",
"vite-build-migration": "vite build --config ./vite.config-migration.mts",
"start": "bun run --watch ./server.ts",
"build": "bun build ./server.ts --outdir=dist --target=node",
"build-all": "bun build ./server.ts --outdir=dist --target=node --minify && javascript-obfuscator ./dist/server.js --output ./dist/taskview.js",
"start-prod": "NODE_ENV=production bun run --watch ./server.ts",
"dev": "bun run --watch ./server.ts",
"start": "NODE_ENV=production node ./dist/taskview-server.js",
"build": "vite build",
"build:docker": "vite build --config ./vite.config.docker.mts",
"build:migration": "vite build --config ./vite.config-migration.mts",
"build:all": "pnpm run build:docker && pnpm run build:migration",
"test": "vitest",
"type-check": "tsc --noEmit",
"lint": "biome lint .",
@@ -21,18 +19,16 @@
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@types/bcrypt": "^5.0.2",
"@types/bun": "latest",
"@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.10.3",
"@types/passport-apple": "^2.0.3",
"@types/pg": "^8.15.5",
"@types/semver": "^7.5.8",
"aws-sdk": "^2.1691.0",
"bytenode": "1.5.6",
"javascript-obfuscator": "^4.1.1",
"mock-aws-s3": "^4.0.2",
"nexe": "^4.0.0-rc.6",
"nock": "^13.5.5",
"vite": "^5.4.9",
"vite-plugin-node": "^4.0.0",
@@ -45,10 +41,14 @@
},
"dependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/passport": "^1.0.17",
"@types/passport-github2": "^1.2.9",
"@types/passport-google-oauth20": "^2.0.17",
"@vitejs/plugin-legacy": "^5.4.2",
"@vitejs/plugin-vue": "^5.1.4",
"axios": "^1.7.7",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.44.4",
@@ -56,6 +56,10 @@
"express": "4.21.0",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"passport": "^0.7.0",
"passport-apple": "^2.0.2",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"pg": "^8.16.3",
"pino": "^9.4.0",
"rotating-file-stream": "^3.2.5",
@@ -63,5 +67,8 @@
"taskview-db-schemas": "workspace:^",
"terser": "^5.36.0",
"zod": "^3.23.8"
},
"engines": {
"node": ">=24 <25"
}
}
}
@@ -0,0 +1,5 @@
#!/bin/bash
VERSION=$1
docker buildx build --platform=linux/amd64,linux/arm64 -t gimanhead/taskview-ce-db-migration:$VERSION -t gimanhead/taskview-ce-db-migration:latest . --load
+2 -2
View File
@@ -1,5 +1,5 @@
FROM node:20-slim
RUN apt-get update && apt-get install -y postgresql-client && apt-get clean
FROM node:24.12.0-alpine
RUN apk add --no-cache postgresql-client
# ENV DB_HOST=localhost
# ENV DB_USER=tvdbuser
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# entrypoint.sh
echo "Waiting for the database service to be ready..."
# Wait for the database service to be ready
until PGPASSWORD=$DB_PASSWORD psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -c '\q'; do
>&2 echo "Postgres is unavailable - sleeping"
sleep 2
done
>&2 echo "Postgres is up - executing command"
# Check if tables exist in the 'tasks' schema using SQL query.
# 'count(*)' will return 0 if there are no tables.
# We read this result and make a decision.
TABLE_COUNT=$(PGPASSWORD=$DB_PASSWORD psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'tasks';" | xargs)
if [ "$TABLE_COUNT" -eq 0 ]; then
echo "No tables found in the 'tasks' schema. Running migration with --create."
node /app-migration/taskview-db-migration.js --create
else
echo "Tables already exist in the 'tasks' schema ($TABLE_COUNT tables found). Running standard migration."
node /app-migration/taskview-db-migration.js
fi
# Check the exit code of the migration script
if [ $? -ne 0 ]; then
>&2 echo "Migration failed! Exiting with an error."
exit 1
else
>&2 echo "Migration successful!"
exit 0
fi
+3 -4
View File
@@ -1,9 +1,8 @@
{
"name": "taskview-ce-api-server",
"version": "1.17.0",
"scripts": {
"byte": "bytenode --compile ./taskview-server-api.js",
"start": "pm2-runtime start ecosystem.config.js --env production"
},
"dependencies": {
"bytenode": "1.5.6"
}
"dependencies": {}
}
+33 -3
View File
@@ -4,8 +4,20 @@ import helmet from 'helmet';
import { appUserMiddleware } from './middlewares/app-user-middleware';
import errorHandler from './middlewares/error-handler';
import routes from './routes';
import passport, { initPassportLogin } from './tv-modules/auth/strategies/passport-login';
import cookieParser from 'cookie-parser';
const VRS = '1.18.0';
const allow = new Set([
...(process.env.CORS_REMOVE_DEFAULT_ALLOWED_ORIGINS === 'true' ? [] : [
// default allowed origins for official TaskView apps
"https://app.taskview.tech",
"https://taskview.handscream.com",
"capacitor://taskview.handscream.com",
"capacitor://app.taskview.tech",
"https://appleid.apple.com"
]),
...(process.env.CORS_ALLOWED_ORIGINS?.split(',') || []),
]);
export default class App {
public app: express.Application;
@@ -15,11 +27,20 @@ export default class App {
this.app = express();
this.port = port;
this.extendApp();
this.initializeMiddlewares();
this.initializeRoutes();
this.app.use(errorHandler);
this.app.use(passport.initialize());
initPassportLogin();
this.extendMiddlewares();
}
protected extendApp(): void { }
protected extendMiddlewares(): void { }
private initializeMiddlewares() {
//add tvJson method, clien need response format like {response: data}
this.app.use((_req: Request, res: Response, next) => {
@@ -29,8 +50,18 @@ export default class App {
next();
});
this.app.use(cookieParser());
this.app.use(appUserMiddleware);
this.app.use(cors());
this.app.use(cors({
credentials: true,
origin(origin, cb) {
if (!origin) return cb(null, true);
if (allow.has(origin)) return cb(null, true);
return cb(new Error(`CORS blocked origin: ${origin}`), false);
},
}));
this.app.use(helmet());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
@@ -45,7 +76,6 @@ export default class App {
public listen() {
return this.app.listen(this.port, '0.0.0.0', () => {
console.log(`Server is running on port ${this.port}`);
console.log(`Server version is ${VRS}`);
});
}
}
+23
View File
@@ -282,5 +282,28 @@
"description": [
"Release 1.16.0"
]
},
"22": {
"version": "1.18.2",
"name": "Release 1.18.2",
"releaseDate": "20260108",
"scripts": [
"/1.18.2/0.1.18.2.sql",
"/1.18.2/all-triggers.sql"
],
"description": [
"Release 1.18.2"
]
},
"23": {
"version": "1.20.0",
"name": "Release 1.20.0",
"releaseDate": "20260221",
"scripts": [
"/1.20.0/all-triggers.sql"
],
"description": [
"Validate tag and task belong to the same project on insert into tasks_to_tags"
]
}
}
@@ -0,0 +1,41 @@
--Delete orphaned records from history.tasks_tasks table
delete from history.tasks_tasks h
where not exists (
select 1
from tasks.tasks t
where t.id = h.task_id
);
--Delete orphaned records from history.tasks_goal_lists table
delete from history.tasks_goal_lists h
where not exists (
select 1
from tasks.goal_lists gl
where gl.id = h.goal_list_id
);
--Delete orphaned records from history.tasks_goals table
delete from history.tasks_goals h
where not exists (
select 1
from tasks.goals g
where g.id = h.goal_id
);
alter table history.tasks_tasks
add constraint fk_history_tasks_tasks_task_id
foreign key (task_id)
references tasks.tasks (id) on delete cascade;
alter table history.tasks_goal_lists
add constraint fk_history_tasks_goal_lists_goal_list_id
foreign key (goal_list_id)
references tasks.goal_lists (id) on delete cascade;
alter table history.tasks_goals
add constraint fk_history_tasks_goals_goal_id
foreign key (goal_id)
references tasks.goals (id) on delete cascade;
@@ -0,0 +1,557 @@
--1.
--Trigger set previous version
create or replace function app.trigger_set_previous_version()
returns trigger as
$date_complete$
begin
new.prev_version = old.version;
return new;
end;
$date_complete$
language plpgsql;
drop trigger if exists trigger_set_previous_version on app.version;
create trigger trigger_set_previous_version
before insert
on app.version
for each row
execute procedure app.trigger_set_previous_version();
--2.
--Trigger for adding owner for taskList from goal
create or replace function tasks.trigger_set_owner_for_component()
returns trigger as
$date_complete$
begin
new.owner = (select owner from tasks.goals where id = new.goal_id);
return new;
end;
$date_complete$
language plpgsql;
drop trigger if exists trigger_set_owner_for_component on tasks.goal_lists;
create trigger trigger_set_owner_for_component
before insert
on tasks.goal_lists
for each row
execute procedure tasks.trigger_set_owner_for_component();
--3.
--Trigger for updating date_complete for task
create or replace function tasks.update_date_complete()
returns trigger as
$date_complete$
begin
if new.complete != old.complete
then
if new.complete = true
then
update tasks.tasks set date_complete = now() where id = old.id;
else
update tasks.tasks set date_complete = null where id = old.id;
end if;
end if;
return new;
end;
$date_complete$
language plpgsql;
drop trigger if exists tr_update_date_complete on tasks.tasks;
create trigger tr_update_date_complete
after update
on tasks.tasks
for each row
execute procedure tasks.update_date_complete();
--4.
-- Delete user from collaboration if not assigned to any goal
create or replace function collaboration.delete_user_if_not_assigned_to_goal()
returns trigger as $$
declare
count int;
begin
if not exists (
select 1
from collaboration.users_to_goals
where user_id = old.user_id
limit 1
) then
delete from collaboration.users where id = old.user_id;
end if;
return old;
end;
$$ language plpgsql;
drop trigger if exists trigger_delete_user_if_not_assigned_to_goal on collaboration.users_to_goals;
create trigger trigger_delete_user_if_not_assigned_to_goal
after delete
on collaboration.users_to_goals
for each row
execute function collaboration.delete_user_if_not_assigned_to_goal();
--5.
--Trigger for checking task graph relation goal to avoid connection between tasks from different goals
create or replace function tasks.check_task_graph_relation_goal()
returns trigger as $$
declare
from_goal int;
to_goal int;
begin
select goal_id into from_goal from tasks.tasks where id = new.from_task_id;
select goal_id into to_goal from tasks.tasks where id = new.to_task_id;
if from_goal is null or to_goal is null then
raise exception 'Invalid task reference in relation';
end if;
if from_goal <> to_goal then
raise exception 'Relation goal_id must match both tasks'' goal_id';
end if;
new.goal_id := from_goal;
return new;
end;
$$ language plpgsql;
drop trigger if exists trigger_task_relation_goal on tasks.task_relations;
create trigger trigger_task_relation_goal
before insert or update on tasks.task_relations
for each row execute function tasks.check_task_graph_relation_goal();
--6.
--Trigger for logging changes in taskList to history table
create or replace function tasks.log_changes_tasks_goal_lists()
returns trigger as
$body$
begin
if tg_op = 'DELETE' then
insert into history.tasks_goal_lists (goal_list_id, edit_date, task, deleted) values (old.id, now(), to_jsonb(old), 1);
return old;
elseif tg_op = 'UPDATE' then
insert into history.tasks_goal_lists (goal_list_id, edit_date, task, deleted)
VALUES (old.id, new.date_creation, to_jsonb(old), 0);
new.edit_date = now();
return new;
end if;
end
$body$
language plpgsql;
drop trigger if exists trigger_log_changes_tasks_goal_lists on tasks.goal_lists;
create trigger trigger_log_changes_tasks_goal_lists
before update or delete
on tasks.goal_lists
for each row
execute procedure tasks.log_changes_tasks_goal_lists();
--7.
--Trigger for logging changes in goal to history table
create or replace function tasks.log_changes_tasks_goals()
returns trigger as
$body$
begin
if tg_op = 'DELETE' then
insert into history.tasks_goals (goal_id, edit_date, task, deleted) values (old.id, now(), to_jsonb(old), 1);
return old;
elseif tg_op = 'UPDATE' then
insert into history.tasks_goals (goal_id, edit_date, task, deleted)
VALUES (old.id, new.date_creation, to_jsonb(old), 0);
new.edit_date = now();
return new;
end if;
end
$body$
language plpgsql;
drop trigger if exists trigger_log_changes_tasks_goals on tasks.goals;
create trigger trigger_log_changes_tasks_goals
before update or delete
on tasks.goals
for each row
execute procedure tasks.log_changes_tasks_goals();
--8.
--Trigger for logging changes in task to history table
create or replace function tasks.log_changes_tasks_tasks()
returns trigger as
$body$
begin
if tg_op = 'DELETE' then
insert into history.tasks_tasks (task_id, edit_date, task, deleted) values (old.id, now(), to_jsonb(old), 1);
return old;
elseif tg_op = 'UPDATE' then
insert into history.tasks_tasks (task_id, edit_date, task, deleted)
VALUES (old.id, new.date_creation, to_jsonb(old), 0);
new.edit_date = now();
return new;
end if;
end
$body$
language plpgsql;
drop trigger if exists trigger_log_changes_tasks_tasks on tasks.tasks;
create trigger trigger_log_changes_tasks_tasks
before update or delete
on tasks.tasks
for each row
execute procedure tasks.log_changes_tasks_tasks();
--9.
--Trigger for setting goal_id default for task
CREATE OR REPLACE FUNCTION tasks.set_goal_id_default_for_task()
RETURNS TRIGGER AS
$$
DECLARE
goal_id INT;
BEGIN
SELECT gl.goal_id
INTO goal_id
FROM tasks.goal_lists gl
WHERE gl.id = NEW.goal_list_id;
IF goal_id IS NOT NULL THEN
NEW.goal_id := goal_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists before_insert_set_goal_id_for_task on tasks.tasks;
CREATE TRIGGER before_insert_set_goal_id_for_task
BEFORE INSERT OR UPDATE
ON tasks.tasks
FOR EACH ROW
EXECUTE FUNCTION tasks.set_goal_id_default_for_task();
--10.
--Trigger for adding default roles and permissions for goal
CREATE OR REPLACE FUNCTION tasks.add_roles_and_permissions()
RETURNS TRIGGER AS
$$
DECLARE
editor_role_id INTEGER;
executor_role_id INTEGER;
BEGIN
-- 1. Create role "editor"
INSERT INTO collaboration.roles (name, goal_id)
VALUES ('editor', NEW.id)
RETURNING id INTO editor_role_id;
-- 2. Create role "executor"
INSERT INTO collaboration.roles (name, goal_id)
VALUES ('executor', NEW.id)
RETURNING id INTO executor_role_id;
-- 3. Add permissions for role "editor"
INSERT INTO collaboration.permissions_to_role (role_id, permission_id)
SELECT editor_role_id, id
FROM tv_auth.permissions
WHERE name IN (
'goal_can_watch_content',
'goal_can_edit',
'goal_can_add_task_list',
'goal_can_manage_users',
'component_can_watch_content',
'component_can_edit',
'component_can_delete',
'component_can_add_tasks',
'task_can_edit_deadline',
'task_can_watch_subtasks',
'task_can_watch_note',
'task_can_recovery_history',
'task_can_watch_assigned_users',
'task_can_edit_priority',
'task_can_delete',
'task_can_watch_details',
'task_can_assign_users',
'task_can_add_subtasks',
'task_can_watch_tags',
'task_can_watch_priority',
'task_can_access_history',
'task_can_edit_tags',
'task_can_edit_description',
'task_can_edit_status',
'task_can_edit_note',
'kanban_can_manage',
'kanban_can_view',
'graph_can_manage',
'graph_can_view'
);
-- 4. Add permissions for role "viewver"
INSERT INTO collaboration.permissions_to_role (role_id, permission_id)
SELECT executor_role_id, id
FROM tv_auth.permissions
WHERE name IN (
'goal_can_watch_content',
'component_can_watch_content',
'component_can_add_tasks',
'task_can_watch_subtasks',
'task_can_watch_note',
'task_can_watch_assigned_users',
'task_can_watch_details',
'task_can_add_subtasks',
'task_can_watch_tags',
'task_can_watch_priority'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists add_roles_after_insert on tasks.goals;
CREATE TRIGGER add_roles_after_insert
AFTER INSERT
ON tasks.goals
FOR EACH ROW
EXECUTE FUNCTION tasks.add_roles_and_permissions();
--11.
--Trigger for adjusting start and end dates for task
CREATE OR REPLACE FUNCTION tasks.adjust_start_and_end_dates()
RETURNS TRIGGER AS
$$
DECLARE
start_timestamp TIMESTAMPTZ;
end_timestamp TIMESTAMPTZ;
BEGIN
-- If start_date is NULL, then start_time should be NULL
IF NEW.start_date IS NULL THEN
NEW.start_time := NULL;
END IF;
-- If end_date is NULL, then end_time should be NULL
IF NEW.end_date IS NULL THEN
NEW.end_time := NULL;
END IF;
-- If both dates are set
IF NEW.start_date IS NOT NULL AND NEW.end_date IS NOT NULL THEN
-- Adjust dates
IF NEW.start_date > NEW.end_date THEN
-- If start_date is greater than end_date, set end_date to start_date
NEW.end_date := NEW.start_date;
-- end_time remains unchanged
ELSIF NEW.end_date < NEW.start_date THEN
-- If end_date is less than start_date, set start_date to end_date
NEW.start_date := NEW.end_date;
-- start_time remains unchanged
END IF;
-- Prepare timestamps for comparison
start_timestamp := (NEW.start_date::text || ' ' || COALESCE(NEW.start_time::text, '00:00:00+00'))::timestamptz;
end_timestamp := (NEW.end_date::text || ' ' || COALESCE(NEW.end_time::text, '00:00:00+00'))::timestamptz;
-- If start_timestamp is greater than end_timestamp, adjust end_date and end_time
IF start_timestamp > end_timestamp THEN
NEW.end_date := NEW.start_date;
-- Assign end_time only if start_time is not NULL
IF NEW.start_time IS NOT NULL AND NEW.end_time IS NOT NULL THEN
NEW.end_time := NEW.start_time;
END IF;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists adjust_dates_and_times_trigger on tasks.tasks;
CREATE TRIGGER adjust_dates_and_times_trigger
BEFORE INSERT OR UPDATE
ON tasks.tasks
FOR EACH ROW
EXECUTE FUNCTION tasks.adjust_start_and_end_dates();
--12.
--Trigger for adding self/owner to collaboration table to be able to assign tasks to self
create or replace function tasks.add_self_to_collaboration()
returns trigger as $$
DECLARE
owner_email TEXT;
BEGIN
select email into owner_email
from tv_auth.users
where id = NEW.owner;
if owner_email is not null then
insert into collaboration.users (email) values (owner_email) ON CONFLICT (email) DO NOTHING;
insert into collaboration.users_to_goals (goal_id, user_id) values (NEW.id, (select id from collaboration.users where email = owner_email));
end if;
return NEW;
END;
$$ language plpgsql;
drop trigger if exists add_selt_to_collaboration_trg on tasks.goals;
create trigger add_selt_to_collaboration_trg
after insert on tasks.goals
for each row
execute function tasks.add_self_to_collaboration();
--13.
--Trigger for adding default kanban columns for new goal
CREATE OR REPLACE FUNCTION tasks.kanban_add_default_columns()
RETURNS TRIGGER AS $$
BEGIN
-- Add default columns for new goal
INSERT INTO tasks.statuses (name, goal_id, view_order)
VALUES
('TODO', NEW.id, 1),
('In Progress', NEW.id, 2),
('Done', NEW.id, 3);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS kanban_add_default_columns_trg ON tasks.goals;
CREATE TRIGGER kanban_add_default_columns_trg
AFTER INSERT ON tasks.goals
FOR EACH ROW
EXECUTE FUNCTION tasks.kanban_add_default_columns();
--14.
--Trigger for validating the correct statusId for the inserted value. To avoid assigning a status that does not belong to the goal.
CREATE OR REPLACE FUNCTION tasks.check_task_status_goal()
RETURNS TRIGGER AS $$
BEGIN
-- Check if there is a record in tasks.statuses with the same goal_id
IF NOT EXISTS (
SELECT 1 FROM tasks.statuses s
WHERE s.id = NEW.status_id AND s.goal_id = NEW.goal_id
) THEN
RAISE EXCEPTION 'Status ID % is not valid for goal ID %', NEW.status_id, NEW.goal_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists enforce_task_status_goal on tasks.tasks;
CREATE TRIGGER enforce_task_status_goal
BEFORE INSERT OR UPDATE ON tasks.tasks
FOR EACH ROW
WHEN (NEW.status_id IS NOT NULL)
EXECUTE FUNCTION tasks.check_task_status_goal();
--15.
--Trigger for setting default orders value for task
CREATE OR REPLACE FUNCTION tasks.set_order_value()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.task_order IS NULL THEN
NEW.task_order := NEW.id;
END IF;
IF NEW.kanban_order IS NULL THEN
NEW.kanban_order := NEW.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists set_order_trigger on tasks.tasks;
CREATE TRIGGER set_order_trigger
BEFORE INSERT ON tasks.tasks
FOR EACH ROW
EXECUTE FUNCTION tasks.set_order_value();
--16.
--Trigger for setting default view order for new status
CREATE OR REPLACE FUNCTION tasks.status_set_default_view_order()
RETURNS TRIGGER AS $$
DECLARE
new_view_order INT;
BEGIN
-- Determine the next view_order for the given goal_id
SELECT COALESCE(MAX(view_order), 0) + 1 INTO new_view_order
FROM tasks.statuses
WHERE goal_id = NEW.goal_id;
-- Assign the calculated value to the view_order field
NEW.view_order := new_view_order;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists set_default_status_view_order on tasks.statuses;
CREATE TRIGGER set_default_status_view_order
BEFORE INSERT ON tasks.statuses
FOR EACH ROW
EXECUTE FUNCTION tasks.status_set_default_view_order();
--17.
--Trigger for validating the correct user_id for the inserted value. To avoid assigning a user that does not belong to the goal.
CREATE OR REPLACE FUNCTION tasks_auth.control_user_id_is_from_same_goal_as_task()
RETURNS TRIGGER AS $$
DECLARE
user_exists BOOLEAN;
BEGIN
SELECT EXISTS (
SELECT 1
FROM tasks.tasks tt
LEFT JOIN collaboration.users_to_goals utg ON utg.goal_id = tt.goal_id
WHERE tt.id = NEW.task_id AND utg.user_id = NEW.collab_user_id
) INTO user_exists;
IF NOT user_exists THEN
RAISE EXCEPTION 'User % is not associated with the goal of task %', NEW.collab_user_id, NEW.task_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists trigger_control_user_id_is_from_same_goal_as_task on tasks_auth.task_assignee;
CREATE TRIGGER trigger_control_user_id_is_from_same_goal_as_task
BEFORE INSERT ON tasks_auth.task_assignee
FOR EACH ROW
EXECUTE FUNCTION tasks_auth.control_user_id_is_from_same_goal_as_task();
--18.
--Trigger for adding owner for task, extend owner from goal or taskList
--delete old function with wrong name
drop trigger if exists trigger_set_owner_for_task on tasks.tasks;
drop function if exists tasks.trigger_set_owner_for_task();
CREATE OR REPLACE FUNCTION tasks.fn_set_owner_for_task()
RETURNS TRIGGER AS
$body$
BEGIN
NEW.owner := COALESCE(
(SELECT owner FROM tasks.goal_lists WHERE id = NEW.goal_list_id),
(SELECT owner FROM tasks.goals WHERE id = NEW.goal_id)
);
IF NEW.owner IS NULL THEN
RAISE EXCEPTION 'Can not insert task without owner';
END IF;
RETURN NEW;
END;
$body$
LANGUAGE plpgsql;
drop trigger if exists trigger_set_owner_for_task on tasks.tasks;
create trigger trigger_set_owner_for_task
before insert
on tasks.tasks
for each row
execute procedure tasks.fn_set_owner_for_task();
@@ -0,0 +1,585 @@
--1.
--Trigger set previous version
create or replace function app.trigger_set_previous_version()
returns trigger as
$date_complete$
begin
new.prev_version = old.version;
return new;
end;
$date_complete$
language plpgsql;
drop trigger if exists trigger_set_previous_version on app.version;
create trigger trigger_set_previous_version
before insert
on app.version
for each row
execute procedure app.trigger_set_previous_version();
--2.
--Trigger for adding owner for taskList from goal
create or replace function tasks.trigger_set_owner_for_component()
returns trigger as
$date_complete$
begin
new.owner = (select owner from tasks.goals where id = new.goal_id);
return new;
end;
$date_complete$
language plpgsql;
drop trigger if exists trigger_set_owner_for_component on tasks.goal_lists;
create trigger trigger_set_owner_for_component
before insert
on tasks.goal_lists
for each row
execute procedure tasks.trigger_set_owner_for_component();
--3.
--Trigger for updating date_complete for task
create or replace function tasks.update_date_complete()
returns trigger as
$date_complete$
begin
if new.complete != old.complete
then
if new.complete = true
then
update tasks.tasks set date_complete = now() where id = old.id;
else
update tasks.tasks set date_complete = null where id = old.id;
end if;
end if;
return new;
end;
$date_complete$
language plpgsql;
drop trigger if exists tr_update_date_complete on tasks.tasks;
create trigger tr_update_date_complete
after update
on tasks.tasks
for each row
execute procedure tasks.update_date_complete();
--4.
-- Delete user from collaboration if not assigned to any goal
create or replace function collaboration.delete_user_if_not_assigned_to_goal()
returns trigger as $$
declare
count int;
begin
if not exists (
select 1
from collaboration.users_to_goals
where user_id = old.user_id
limit 1
) then
delete from collaboration.users where id = old.user_id;
end if;
return old;
end;
$$ language plpgsql;
drop trigger if exists trigger_delete_user_if_not_assigned_to_goal on collaboration.users_to_goals;
create trigger trigger_delete_user_if_not_assigned_to_goal
after delete
on collaboration.users_to_goals
for each row
execute function collaboration.delete_user_if_not_assigned_to_goal();
--5.
--Trigger for checking task graph relation goal to avoid connection between tasks from different goals
create or replace function tasks.check_task_graph_relation_goal()
returns trigger as $$
declare
from_goal int;
to_goal int;
begin
select goal_id into from_goal from tasks.tasks where id = new.from_task_id;
select goal_id into to_goal from tasks.tasks where id = new.to_task_id;
if from_goal is null or to_goal is null then
raise exception 'Invalid task reference in relation';
end if;
if from_goal <> to_goal then
raise exception 'Relation goal_id must match both tasks'' goal_id';
end if;
new.goal_id := from_goal;
return new;
end;
$$ language plpgsql;
drop trigger if exists trigger_task_relation_goal on tasks.task_relations;
create trigger trigger_task_relation_goal
before insert or update on tasks.task_relations
for each row execute function tasks.check_task_graph_relation_goal();
--6.
--Trigger for logging changes in taskList to history table
create or replace function tasks.log_changes_tasks_goal_lists()
returns trigger as
$body$
begin
if tg_op = 'DELETE' then
insert into history.tasks_goal_lists (goal_list_id, edit_date, task, deleted) values (old.id, now(), to_jsonb(old), 1);
return old;
elseif tg_op = 'UPDATE' then
insert into history.tasks_goal_lists (goal_list_id, edit_date, task, deleted)
VALUES (old.id, new.date_creation, to_jsonb(old), 0);
new.edit_date = now();
return new;
end if;
end
$body$
language plpgsql;
drop trigger if exists trigger_log_changes_tasks_goal_lists on tasks.goal_lists;
create trigger trigger_log_changes_tasks_goal_lists
before update or delete
on tasks.goal_lists
for each row
execute procedure tasks.log_changes_tasks_goal_lists();
--7.
--Trigger for logging changes in goal to history table
create or replace function tasks.log_changes_tasks_goals()
returns trigger as
$body$
begin
if tg_op = 'DELETE' then
insert into history.tasks_goals (goal_id, edit_date, task, deleted) values (old.id, now(), to_jsonb(old), 1);
return old;
elseif tg_op = 'UPDATE' then
insert into history.tasks_goals (goal_id, edit_date, task, deleted)
VALUES (old.id, new.date_creation, to_jsonb(old), 0);
new.edit_date = now();
return new;
end if;
end
$body$
language plpgsql;
drop trigger if exists trigger_log_changes_tasks_goals on tasks.goals;
create trigger trigger_log_changes_tasks_goals
before update or delete
on tasks.goals
for each row
execute procedure tasks.log_changes_tasks_goals();
--8.
--Trigger for logging changes in task to history table
create or replace function tasks.log_changes_tasks_tasks()
returns trigger as
$body$
begin
if tg_op = 'DELETE' then
insert into history.tasks_tasks (task_id, edit_date, task, deleted) values (old.id, now(), to_jsonb(old), 1);
return old;
elseif tg_op = 'UPDATE' then
insert into history.tasks_tasks (task_id, edit_date, task, deleted)
VALUES (old.id, new.date_creation, to_jsonb(old), 0);
new.edit_date = now();
return new;
end if;
end
$body$
language plpgsql;
drop trigger if exists trigger_log_changes_tasks_tasks on tasks.tasks;
create trigger trigger_log_changes_tasks_tasks
before update or delete
on tasks.tasks
for each row
execute procedure tasks.log_changes_tasks_tasks();
--9.
--Trigger for setting goal_id default for task
CREATE OR REPLACE FUNCTION tasks.set_goal_id_default_for_task()
RETURNS TRIGGER AS
$$
DECLARE
goal_id INT;
BEGIN
SELECT gl.goal_id
INTO goal_id
FROM tasks.goal_lists gl
WHERE gl.id = NEW.goal_list_id;
IF goal_id IS NOT NULL THEN
NEW.goal_id := goal_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists before_insert_set_goal_id_for_task on tasks.tasks;
CREATE TRIGGER before_insert_set_goal_id_for_task
BEFORE INSERT OR UPDATE
ON tasks.tasks
FOR EACH ROW
EXECUTE FUNCTION tasks.set_goal_id_default_for_task();
--10.
--Trigger for adding default roles and permissions for goal
CREATE OR REPLACE FUNCTION tasks.add_roles_and_permissions()
RETURNS TRIGGER AS
$$
DECLARE
editor_role_id INTEGER;
executor_role_id INTEGER;
BEGIN
-- 1. Create role "editor"
INSERT INTO collaboration.roles (name, goal_id)
VALUES ('editor', NEW.id)
RETURNING id INTO editor_role_id;
-- 2. Create role "executor"
INSERT INTO collaboration.roles (name, goal_id)
VALUES ('executor', NEW.id)
RETURNING id INTO executor_role_id;
-- 3. Add permissions for role "editor"
INSERT INTO collaboration.permissions_to_role (role_id, permission_id)
SELECT editor_role_id, id
FROM tv_auth.permissions
WHERE name IN (
'goal_can_watch_content',
'goal_can_edit',
'goal_can_add_task_list',
'goal_can_manage_users',
'component_can_watch_content',
'component_can_edit',
'component_can_delete',
'component_can_add_tasks',
'task_can_edit_deadline',
'task_can_watch_subtasks',
'task_can_watch_note',
'task_can_recovery_history',
'task_can_watch_assigned_users',
'task_can_edit_priority',
'task_can_delete',
'task_can_watch_details',
'task_can_assign_users',
'task_can_add_subtasks',
'task_can_watch_tags',
'task_can_watch_priority',
'task_can_access_history',
'task_can_edit_tags',
'task_can_edit_description',
'task_can_edit_status',
'task_can_edit_note',
'kanban_can_manage',
'kanban_can_view',
'graph_can_manage',
'graph_can_view'
);
-- 4. Add permissions for role "viewver"
INSERT INTO collaboration.permissions_to_role (role_id, permission_id)
SELECT executor_role_id, id
FROM tv_auth.permissions
WHERE name IN (
'goal_can_watch_content',
'component_can_watch_content',
'component_can_add_tasks',
'task_can_watch_subtasks',
'task_can_watch_note',
'task_can_watch_assigned_users',
'task_can_watch_details',
'task_can_add_subtasks',
'task_can_watch_tags',
'task_can_watch_priority'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists add_roles_after_insert on tasks.goals;
CREATE TRIGGER add_roles_after_insert
AFTER INSERT
ON tasks.goals
FOR EACH ROW
EXECUTE FUNCTION tasks.add_roles_and_permissions();
--11.
--Trigger for adjusting start and end dates for task
CREATE OR REPLACE FUNCTION tasks.adjust_start_and_end_dates()
RETURNS TRIGGER AS
$$
DECLARE
start_timestamp TIMESTAMPTZ;
end_timestamp TIMESTAMPTZ;
BEGIN
-- If start_date is NULL, then start_time should be NULL
IF NEW.start_date IS NULL THEN
NEW.start_time := NULL;
END IF;
-- If end_date is NULL, then end_time should be NULL
IF NEW.end_date IS NULL THEN
NEW.end_time := NULL;
END IF;
-- If both dates are set
IF NEW.start_date IS NOT NULL AND NEW.end_date IS NOT NULL THEN
-- Adjust dates
IF NEW.start_date > NEW.end_date THEN
-- If start_date is greater than end_date, set end_date to start_date
NEW.end_date := NEW.start_date;
-- end_time remains unchanged
ELSIF NEW.end_date < NEW.start_date THEN
-- If end_date is less than start_date, set start_date to end_date
NEW.start_date := NEW.end_date;
-- start_time remains unchanged
END IF;
-- Prepare timestamps for comparison
start_timestamp := (NEW.start_date::text || ' ' || COALESCE(NEW.start_time::text, '00:00:00+00'))::timestamptz;
end_timestamp := (NEW.end_date::text || ' ' || COALESCE(NEW.end_time::text, '00:00:00+00'))::timestamptz;
-- If start_timestamp is greater than end_timestamp, adjust end_date and end_time
IF start_timestamp > end_timestamp THEN
NEW.end_date := NEW.start_date;
-- Assign end_time only if start_time is not NULL
IF NEW.start_time IS NOT NULL AND NEW.end_time IS NOT NULL THEN
NEW.end_time := NEW.start_time;
END IF;
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists adjust_dates_and_times_trigger on tasks.tasks;
CREATE TRIGGER adjust_dates_and_times_trigger
BEFORE INSERT OR UPDATE
ON tasks.tasks
FOR EACH ROW
EXECUTE FUNCTION tasks.adjust_start_and_end_dates();
--12.
--Trigger for adding self/owner to collaboration table to be able to assign tasks to self
create or replace function tasks.add_self_to_collaboration()
returns trigger as $$
DECLARE
owner_email TEXT;
BEGIN
select email into owner_email
from tv_auth.users
where id = NEW.owner;
if owner_email is not null then
insert into collaboration.users (email) values (owner_email) ON CONFLICT (email) DO NOTHING;
insert into collaboration.users_to_goals (goal_id, user_id) values (NEW.id, (select id from collaboration.users where email = owner_email));
end if;
return NEW;
END;
$$ language plpgsql;
drop trigger if exists add_selt_to_collaboration_trg on tasks.goals;
create trigger add_selt_to_collaboration_trg
after insert on tasks.goals
for each row
execute function tasks.add_self_to_collaboration();
--13.
--Trigger for adding default kanban columns for new goal
CREATE OR REPLACE FUNCTION tasks.kanban_add_default_columns()
RETURNS TRIGGER AS $$
BEGIN
-- Add default columns for new goal
INSERT INTO tasks.statuses (name, goal_id, view_order)
VALUES
('TODO', NEW.id, 1),
('In Progress', NEW.id, 2),
('Done', NEW.id, 3);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS kanban_add_default_columns_trg ON tasks.goals;
CREATE TRIGGER kanban_add_default_columns_trg
AFTER INSERT ON tasks.goals
FOR EACH ROW
EXECUTE FUNCTION tasks.kanban_add_default_columns();
--14.
--Trigger for validating the correct statusId for the inserted value. To avoid assigning a status that does not belong to the goal.
CREATE OR REPLACE FUNCTION tasks.check_task_status_goal()
RETURNS TRIGGER AS $$
BEGIN
-- Check if there is a record in tasks.statuses with the same goal_id
IF NOT EXISTS (
SELECT 1 FROM tasks.statuses s
WHERE s.id = NEW.status_id AND s.goal_id = NEW.goal_id
) THEN
RAISE EXCEPTION 'Status ID % is not valid for goal ID %', NEW.status_id, NEW.goal_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists enforce_task_status_goal on tasks.tasks;
CREATE TRIGGER enforce_task_status_goal
BEFORE INSERT OR UPDATE ON tasks.tasks
FOR EACH ROW
WHEN (NEW.status_id IS NOT NULL)
EXECUTE FUNCTION tasks.check_task_status_goal();
--15.
--Trigger for setting default orders value for task
CREATE OR REPLACE FUNCTION tasks.set_order_value()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.task_order IS NULL THEN
NEW.task_order := NEW.id;
END IF;
IF NEW.kanban_order IS NULL THEN
NEW.kanban_order := NEW.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists set_order_trigger on tasks.tasks;
CREATE TRIGGER set_order_trigger
BEFORE INSERT ON tasks.tasks
FOR EACH ROW
EXECUTE FUNCTION tasks.set_order_value();
--16.
--Trigger for setting default view order for new status
CREATE OR REPLACE FUNCTION tasks.status_set_default_view_order()
RETURNS TRIGGER AS $$
DECLARE
new_view_order INT;
BEGIN
-- Determine the next view_order for the given goal_id
SELECT COALESCE(MAX(view_order), 0) + 1 INTO new_view_order
FROM tasks.statuses
WHERE goal_id = NEW.goal_id;
-- Assign the calculated value to the view_order field
NEW.view_order := new_view_order;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists set_default_status_view_order on tasks.statuses;
CREATE TRIGGER set_default_status_view_order
BEFORE INSERT ON tasks.statuses
FOR EACH ROW
EXECUTE FUNCTION tasks.status_set_default_view_order();
--17.
--Trigger for validating the correct user_id for the inserted value. To avoid assigning a user that does not belong to the goal.
CREATE OR REPLACE FUNCTION tasks_auth.control_user_id_is_from_same_goal_as_task()
RETURNS TRIGGER AS $$
DECLARE
user_exists BOOLEAN;
BEGIN
SELECT EXISTS (
SELECT 1
FROM tasks.tasks tt
LEFT JOIN collaboration.users_to_goals utg ON utg.goal_id = tt.goal_id
WHERE tt.id = NEW.task_id AND utg.user_id = NEW.collab_user_id
) INTO user_exists;
IF NOT user_exists THEN
RAISE EXCEPTION 'User % is not associated with the goal of task %', NEW.collab_user_id, NEW.task_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
drop trigger if exists trigger_control_user_id_is_from_same_goal_as_task on tasks_auth.task_assignee;
CREATE TRIGGER trigger_control_user_id_is_from_same_goal_as_task
BEFORE INSERT ON tasks_auth.task_assignee
FOR EACH ROW
EXECUTE FUNCTION tasks_auth.control_user_id_is_from_same_goal_as_task();
--18.
--Trigger for adding owner for task, extend owner from goal or taskList
--delete old function with wrong name
drop trigger if exists trigger_set_owner_for_task on tasks.tasks;
drop function if exists tasks.trigger_set_owner_for_task();
CREATE OR REPLACE FUNCTION tasks.fn_set_owner_for_task()
RETURNS TRIGGER AS
$body$
BEGIN
NEW.owner := COALESCE(
(SELECT owner FROM tasks.goal_lists WHERE id = NEW.goal_list_id),
(SELECT owner FROM tasks.goals WHERE id = NEW.goal_id)
);
IF NEW.owner IS NULL THEN
RAISE EXCEPTION 'Can not insert task without owner';
END IF;
RETURN NEW;
END;
$body$
LANGUAGE plpgsql;
drop trigger if exists trigger_set_owner_for_task on tasks.tasks;
create trigger trigger_set_owner_for_task
before insert
on tasks.tasks
for each row
execute procedure tasks.fn_set_owner_for_task();
--19.
--Trigger for validating that tag and task belong to the same project (goal_id)
drop trigger if exists trigger_check_tag_task_same_goal on tasks.tasks_to_tags;
drop function if exists tasks.check_tag_task_same_goal();
create or replace function tasks.check_tag_task_same_goal()
returns trigger as $$
declare
v_tag_goal_id integer;
v_task_goal_id integer;
begin
select goal_id into v_tag_goal_id from tasks.tags where id = new.tag_id;
select goal_id into v_task_goal_id from tasks.tasks where id = new.task_id;
if v_tag_goal_id is null or v_tag_goal_id != v_task_goal_id then
raise exception 'Tag (id=%) and task (id=%) belong to different projects', new.tag_id, new.task_id;
end if;
return new;
end;
$$ language plpgsql;
drop trigger if exists trigger_check_tag_task_same_goal on tasks.tasks_to_tags;
create trigger trigger_check_tag_task_same_goal
before insert on tasks.tasks_to_tags
for each row
execute function tasks.check_tag_task_same_goal();
+125 -23
View File
@@ -16,12 +16,15 @@ import {
import { generateString, isEmail, time } from '../../utils/helpers';
import EnEmailTemplate from './mail/confirm-email-en';
import RuEmailTemplate from './mail/confirm-email-ru';
import type { ExternalAuthUser } from './strategies/external-auth.types';
export default class AuthController {
private readonly jwtAlg: Algorithm = process.env.JWT_ALG as Algorithm;
private readonly jwtExp: string = process.env.ACCESS_LIFE_TIME!;
private readonly jwtRefreshExp: string = process.env.REFRESH_LIFE_TIME!;
private readonly refreshTokenCookieName: string = 'taskview-refresh';
comparePasswords(pwd: string, hash: string): Promise<boolean> {
return new Promise((resolve) => {
//Prev version was written in PHP need to replace
@@ -91,6 +94,9 @@ export default class AuthController {
return this.makeidLogin(16);
}
generateLoginCode() {
return `${this.makeidLogin(12)}:${Date.now()}`.toLocaleLowerCase();
}
/**
* Register user by email and send login code to the email
* @param req
@@ -110,7 +116,7 @@ export default class AuthController {
const { email } = data.data;
const code = `${this.makeidLogin(12)}:${Date.now()}`.toLocaleLowerCase();
const code = this.generateLoginCode();
$logger.info(data.data, `[AuthController:sendLoginCode] we got data for send login code`);
@@ -175,6 +181,86 @@ export default class AuthController {
});
}
loginByProvider = async (req: Request, res: Response) => {
const user = req.user as ExternalAuthUser;
if (!user) {
return res.status(400).send('User not found');
}
let userData = await req.appUser.authManager.repository.getUserByLogin(
user.email,
isEmail(user.email)
);
if (!userData) {
const password = this.makeidLogin(7);
const login = this.makeidLogin(7);
const id = await req.appUser.authManager.repository.registerUserInDb({
login,
email: user.email,
password: hashSync(password, 10),
block: 0,
confirmEmailCode: '',
});
if (!id) {
$logger.error(`Can not register user ${user.email} & login ${login}`);
return res.status(500).send(`Can not register user ${user.email} & login ${login}`);
}
userData = await req.appUser.authManager.repository.getUserByLogin(
user.email,
isEmail(user.email)
);
}
if (!userData) {
$logger.error(`Can not find user ${user.email} after registration`);
return res.status(500).send(`Can not find user ${user.email} after registration`);
}
const code = this.generateLoginCode();
const result = await req.appUser.authManager.repository.updateLoginCode(code, userData.email);
if (!result) {
$logger.error(`Can not update login code for user ${userData.email}`);
return res.status(500).send(`Can not update login code for user`);
}
const authData = {
code: code.split(':')[0],
email: userData.email,
};
const encodedAuthData = encodeURIComponent(JSON.stringify(authData));
try {
const platformData = JSON.parse(req.query.state as string);
if (platformData.platform === "mobile") {
return res.redirect(
`taskview://login?tokens=${encodedAuthData}`
);
}
} catch (error) {
$logger.info(`Can not parse platform data from state: ${req.query.state}`);
}
return res.redirect(`${process.env.APP_URL}/login?tokens=${encodedAuthData}`);
}
setRefreshToken = async (res: Response, refreshToken: string) => {
res.cookie(this.refreshTokenCookieName, refreshToken, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 1000 * 60 * 60 * 24 * 30,
});
}
loginByCode = async (req: Request, res: Response) => {
const schema = z.object({
email: z.string().email().toLowerCase(),
@@ -202,7 +288,12 @@ export default class AuthController {
}
if (tokenFromDb[0] !== data.data.code) {
return res.status(400).end();
return res.status(400).send({ message: 'Invalid code' });
}
if (tokenFromDb[1] && Date.now() - +tokenFromDb[1] > 60 * 1000) {
await req.appUser.authManager.repository.updateLoginCode(null, userData.email);
return res.status(400).send({ message: 'Code expired, get new code' });
}
const tokenRowId = await req.appUser.authManager.jwtStorage.initTokenRecord(userData.id);
@@ -225,11 +316,9 @@ export default class AuthController {
$logger.error(`Can not update tokens in JWT Storage for user ${userData.id} and rowId ${tokenRowId}`);
}
try {
await req.appUser.authManager.repository.updateLoginCode(null, userData.email);
} catch (_err: unknown) {
$logger.error(`Can not update updateLoginCode after login`);
}
await req.appUser.authManager.repository.updateLoginCode(null, userData.email);
await this.setRefreshToken(res, tokens.refresh);
return res.json(tokens);
};
@@ -276,6 +365,8 @@ export default class AuthController {
$logger.error(`Can not update tokens in JWT Storage for user ${userData.id} and rowId ${tokenRowId}`);
}
await this.setRefreshToken(res, tokens.refresh);
return res.json(tokens);
}
@@ -325,7 +416,7 @@ export default class AuthController {
emailTemplate = EnEmailTemplate;
}
const confirmUrl = `https://apitaskview.handscream.com/module/auth/confirm/email/${confirmEmailCode}/login/${login}`;
const confirmUrl = `https://${process.env.APP_URL}/module/auth/confirm/email/${confirmEmailCode}/login/${login}`;
if (emailTemplate) {
confirmEmailBody = emailTemplate.replace('{link}', confirmUrl);
@@ -473,37 +564,46 @@ export default class AuthController {
};
logout = async (req: Request, res: Response) => {
if (!req.headers['authorization']) {
return res.status(400).end();
}
this.setRefreshToken(res, '');
const result = req.headers['authorization'].match(/Bearer\s(\S+)/);
const result = req.headers['authorization']?.match(/Bearer\s(\S+)/);
if (!result) {
return res.status(400).end();
return res.status(401).send({ message: 'Unauthorized' });
}
const payload = decode(result['1']) as UserJwtPayload;
if (!payload) {
return res.status(400).end();
const tokenId = req.appUser.getTokenId();
if (!tokenId) {
return res.status(401).send({ message: 'Unauthorized' });
}
const deleteResult = await req.appUser.authManager.jwtStorage.deleteTokens(payload.userData.id, result['1']);
const deleteResult = await req.appUser.authManager.jwtStorage.deleteTokens(tokenId, result['1']);
if (!deleteResult) {
return res.status(500).end();
return res.status(500).send({ message: 'Failed to revoke token' });
}
return res.send();
return res.status(204).end();
};
refreshTokens = async (req: Request, res: Response) => {
const refreshData = RefreshTokenSchema.safeParse(req.body);
if (!refreshData.success) {
return res.status(400).end();
let refreshToken = req.cookies[this.refreshTokenCookieName];
if (!refreshToken) {
const refreshData = RefreshTokenSchema.safeParse(req.body);
if (!refreshData.success) {
return res.status(400).send({ message: 'Invalid refresh token' });
}
refreshToken = refreshData.data.refreshToken;
$logger.info(`Refresh token found in body`);
} else {
$logger.info(`Refresh token found in cookies`);
}
const payload = await AuthController.validateTokens(refreshData.data.refreshToken);
const payload = await AuthController.validateTokens(refreshToken);
if (!payload) {
return res.status(400).end();
@@ -522,6 +622,8 @@ export default class AuthController {
return res.status(500).end();
}
await this.setRefreshToken(res, newTokens.refresh);
return res.json(newTokens);
};
+1
View File
@@ -42,6 +42,7 @@ export default class AuthModel {
return !!(data.rowCount && data.rowCount > 0);
} catch (error: unknown) {
$logger.error(error, 'Can not update login code');
return false;
}
return true;
+29 -2
View File
@@ -1,8 +1,9 @@
import { Router } from 'express';
import { Router, type NextFunction, type Request, type Response } from 'express';
import type { Routable } from '../../types/routable.type';
import AuthController from './AuthController';
import { IsLoggedIn } from './middlewares/is-logged-in';
import passport from './strategies/passport-login';
import { ExternalProviderScope } from './strategies/external-auth.types';
export default class AuthRoutes implements Routable {
private readonly router: ReturnType<typeof Router>;
private readonly authController: AuthController;
@@ -29,5 +30,31 @@ export default class AuthRoutes implements Routable {
this.router.post('/refresh/token', this.authController.refreshTokens);
this.router.post('/delete/account/code', [IsLoggedIn], this.authController.sendDeleteAccountCode);
this.router.post('/delete/account', [IsLoggedIn], this.authController.deleteUserAccaunt);
this.router.get(
'/provider/:providerName',
(req: Request, res: Response, next: NextFunction) => passport.authenticate(req.params.providerName, {
scope: ExternalProviderScope[req.params.providerName],
session: false,
state: JSON.stringify({
platform: req.query.platform || '',
})
})(req, res, next)
);
this.router.get(
'/provider/:providerName/callback',
(req: Request, res: Response, next: NextFunction) => passport.authenticate(req.params.providerName, {
scope: ExternalProviderScope[req.params.providerName], session: false
})(req, res, next),
this.authController.loginByProvider
);
this.router.post(
'/provider/:providerName/callback',
(req: Request, res: Response, next: NextFunction) => passport.authenticate(req.params.providerName, {
scope: ExternalProviderScope[req.params.providerName], session: false
})(req, res, next),
this.authController.loginByProvider
);
}
}
+7 -7
View File
@@ -20,11 +20,11 @@ export default class JwtStorage {
}
return false;
} catch (error: any) {
$logger.error('Can not complete initTokenRecord', {
$logger.error({
userId,
errorMessage: error.message,
errorStack: error.stack,
});
}, 'Can not complete initTokenRecord');
return false;
}
}
@@ -40,7 +40,7 @@ export default class JwtStorage {
const res = await this.db.query(query, [accessToken, refreshToken, rowId]);
return !!(res.rowCount && res.rowCount > 0);
} catch (error: any) {
$logger.error('Error updating tokens:', { errorMessage: error.message, errorStack: error.stack });
$logger.error({ errorMessage: error.message, errorStack: error.stack }, 'Error updating tokens:');
return false;
}
}
@@ -55,11 +55,11 @@ export default class JwtStorage {
return false;
} catch (error: any) {
$logger.error('Error fetching tokens', {
$logger.error({
rowId,
errorMessage: error.message,
errorStack: error.stack,
});
}, 'Error fetching tokens');
return false;
}
}
@@ -67,8 +67,8 @@ export default class JwtStorage {
async deleteTokens(userId: number, accessToken: string): Promise<boolean> {
try {
const query = 'DELETE FROM tv_auth.user_tokens WHERE user_id = $1 AND access_token = $2;';
const deleteResul = await this.db.query(query, [userId, accessToken]);
return !!(deleteResul.rowCount && deleteResul.rowCount > 0);
await this.db.query(query, [userId, accessToken]);
return true;
} catch (_error: any) {
return false;
}
@@ -0,0 +1,50 @@
import passport from "passport";
import { Strategy as AppleStrategy } from "passport-apple";
import jwt from "jsonwebtoken";
import { $logger } from "../../../modules/logget";
import type { ExternalAuthUser } from "./external-auth.types";
interface AppleDecodedToken {
email?: string;
email_verified?: boolean | string;
sub?: string;
}
export function initAppleStrategy() {
if (!process.env.APPLE_CLIENT_ID ||
!process.env.APPLE_TEAM_ID ||
!process.env.APPLE_KEY_ID ||
!process.env.APPLE_CALLBACK_URL ||
!process.env.APPLE_KEY_LOCATION) {
$logger.warn("APPLE_CLIENT_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_CALLBACK_URL, and APPLE_KEY_LOCATION must be set");
console.warn("APPLE_CLIENT_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_CALLBACK_URL, and APPLE_KEY_LOCATION must be set");
return;
}
const options = {
clientID: process.env.APPLE_CLIENT_ID,
teamID: process.env.APPLE_TEAM_ID,
callbackURL: process.env.APPLE_CALLBACK_URL,
keyID: process.env.APPLE_KEY_ID,
privateKeyLocation: process.env.APPLE_KEY_LOCATION,
scope: ["name", "email"],
passReqToCallback: true as const,
};
passport.use(new AppleStrategy(options, async (_req, _accessToken, _refreshToken, idToken, _profile, done) => {
try {
const decoded = jwt.decode(idToken) as AppleDecodedToken | null;
const email = decoded?.email;
if (!email) return done(null, undefined);
const user: ExternalAuthUser = {
email,
provider: "apple",
};
done(null, user);
} catch (e) {
done(e instanceof Error ? e : new Error(String(e)));
}
}));
}
@@ -0,0 +1,10 @@
export type ExternalAuthUser = {
email: string;
provider: 'google' | 'github' | string;
}
export const ExternalProviderScope: Record<ExternalAuthUser['provider'], string[]> = {
google: ["email"],
github: ["user:email"],
apple: ["email"],
}
@@ -0,0 +1,37 @@
import passport from "passport";
import { Strategy as GitHubStrategy } from "passport-github2";
import { $logger } from "../../../modules/logget";
import type { ExternalAuthUser } from "./external-auth.types";
import type { Profile } from "passport-github2";
import type { VerifyCallback } from "passport-google-oauth20";
export function initGithubStrategy() {
if (!process.env.GITHUB_CLIENT_ID || !process.env.GITHUB_CLIENT_SECRET || !process.env.GITHUB_CALLBACK_URL) {
$logger.warn("GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, and GITHUB_CALLBACK_URL must be set");
console.warn("GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, and GITHUB_CALLBACK_URL must be set");
return;
}
const options = {
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: process.env.GITHUB_CALLBACK_URL,
scope: ["user:email"],
}
passport.use(new GitHubStrategy(options, async (_accessToken: string, _refreshToken: string, profile: Profile, done: VerifyCallback) => {
try {
const email = profile.emails?.[0]?.value;
if (!email) return done(null, false);
const user: ExternalAuthUser = {
email,
provider: "github",
};
done(null, user);
} catch (e) {
done(e);
}
}));
}
@@ -0,0 +1,33 @@
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { $logger } from "../../../modules/logget";
import type { ExternalAuthUser } from "./external-auth.types";
export function initGoogleStrategy() {
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET || !process.env.GOOGLE_CALLBACK_URL) {
$logger.warn("GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_CALLBACK_URL must be set");
console.warn("GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_CALLBACK_URL must be set");
return;
}
const options = {
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
}
passport.use(new GoogleStrategy(options, async (_accessToken, _refreshToken, profile, done) => {
try {
const email = profile.emails?.[0]?.value;
if (!email || !profile._json.email_verified) return done(null, false);
const user: ExternalAuthUser = {
email,
provider: "google",
};
done(null, user);
} catch (e) {
done(e);
}
}));
}
@@ -0,0 +1,13 @@
import passport from "passport";
import { initGoogleStrategy } from "./google.strategy";
import { initGithubStrategy } from "./github.strategy";
import { initAppleStrategy } from "./apple.strategy";
export function initPassportLogin() {
initGoogleStrategy();
initGithubStrategy();
initAppleStrategy();
}
export default passport;
+1 -1
View File
@@ -12,7 +12,7 @@ export class StartManager {
constructor(user: AppUser) {
this.user = user;
this.repository = new StartRepository();
this.repository = new StartRepository(this.user);
}
async fetchAllLists() {
+13 -10
View File
@@ -7,11 +7,14 @@ import type { TagToTaskInDb } from '../tags/tags.types';
import { TaskItemForClient } from '../tasks/TaskItemForClient';
import type { AssigneesForTaskFromDb, FetchAllListsResult, UsersByProjectsFromDb } from './start.types';
//TODO: refactor
export class StartRepository {
public readonly db: Database;
public readonly user: AppUser;
constructor() {
constructor(user: AppUser) {
this.db = Database.getInstance();
this.user = user;
}
async fetchAllLists(user: AppUser): Promise<FetchAllListsResult[] | false> {
@@ -107,7 +110,7 @@ export class StartRepository {
async fetchAllActiveTasksForGoals(
goalsIds: number[],
assignees?: AssigneesForTaskFromDb[]
assignees?: AssigneesForTaskFromDb[],
): Promise<TaskItemForClient[]> {
if (goalsIds.length === 0) {
return [];
@@ -136,9 +139,9 @@ export class StartRepository {
const tagsQuery = `select ttt.*
from tasks.tags
left join tasks.tasks_to_tags ttt on tags.id = ttt.tag_id
where owner = 1 and goal_id in (${placeholders.join(',')});`;
where goal_id in (${placeholders.join(',')}) and owner = $${goalsIds.length + 1};`;
const tagsResult = await this.db.query<TagToTaskInDb>(tagsQuery, goalsIds);
const tagsResult = await this.db.query<TagToTaskInDb>(tagsQuery, [...goalsIds, this.user.getUserData()?.id]);
if (tagsResult) {
tagsResult.rows.forEach((r) => {
@@ -209,9 +212,9 @@ export class StartRepository {
const tagsQuery = `select ttt.*
from tasks.tags
left join tasks.tasks_to_tags ttt on tags.id = ttt.tag_id
where owner = 1 and goal_id in (${placeholders.join(',')});`;
where goal_id in (${placeholders.join(',')}) and owner = $${goalsIds.length + 1};`;
const tagsResult = await this.db.query<TagToTaskInDb>(tagsQuery, goalsIds);
const tagsResult = await this.db.query<TagToTaskInDb>(tagsQuery, [...goalsIds, this.user.getUserData()?.id]);
if (tagsResult) {
tagsResult.rows.forEach((r) => {
@@ -287,9 +290,9 @@ export class StartRepository {
const tagsQuery = `select ttt.*
from tasks.tags
left join tasks.tasks_to_tags ttt on tags.id = ttt.tag_id
where owner = 1 and goal_id in (${placeholders.join(',')});`;
where goal_id in (${placeholders.join(',')}) and owner = $${goalsIds.length + 1};`;
const tagsResult = await this.db.query<TagToTaskInDb>(tagsQuery, goalsIds);
const tagsResult = await this.db.query<TagToTaskInDb>(tagsQuery, [...goalsIds, this.user.getUserData()?.id]);
if (tagsResult) {
tagsResult.rows.forEach((r) => {
@@ -350,9 +353,9 @@ export class StartRepository {
const tagsQuery = `select ttt.*
from tasks.tags
left join tasks.tasks_to_tags ttt on tags.id = ttt.tag_id
where owner = 1 and goal_id in (${placeholders.join(',')});`;
where goal_id in (${placeholders.join(',')}) and owner = $${goalsIds.length + 1};`;
const tagsResult = await this.db.query<TagToTaskInDb>(tagsQuery, goalsIds);
const tagsResult = await this.db.query<TagToTaskInDb>(tagsQuery, [...goalsIds, this.user.getUserData()?.id]);
if (tagsResult) {
tagsResult.rows.forEach((r) => {
+14 -1
View File
@@ -1,6 +1,6 @@
import { and, eq, inArray } from 'drizzle-orm';
import type { TagsSchemaTypeForSelect } from 'taskview-db-schemas';
import { GoalsSchema, TagsSchema, TasksToTagsSchema } from 'taskview-db-schemas';
import { GoalsSchema, TagsSchema, TasksSchema, TasksToTagsSchema } from 'taskview-db-schemas';
import type { AppUser } from '../../core/AppUser';
import { Database } from '../../modules/db';
import { GoalPermissions } from '../../types/auth.types';
@@ -171,6 +171,19 @@ export class TagsRepository {
}
async toggleTagNew(tagId: number, taskId: number): Promise<'delete' | 'add' | null> {
const [tag, task] = await Promise.all([
callWithCatch(() =>
this.db.dbDrizzle.select({ goalId: TagsSchema.goalId }).from(TagsSchema).where(eq(TagsSchema.id, tagId))
),
callWithCatch(() =>
this.db.dbDrizzle.select({ goalId: TasksSchema.goalId }).from(TasksSchema).where(eq(TasksSchema.id, taskId))
),
])
if (!tag?.[0] || !task?.[0] || tag[0].goalId === null || tag[0].goalId !== task[0].goalId) {
return null
}
const tagExists = await this.tagExists(tagId, taskId);
if (tagExists) {
+6 -4
View File
@@ -274,7 +274,7 @@ export class TasksRepository {
async fetchSubtasks(taskId: number): Promise<TasksSchemaTypeForSelect[]> {
const result = await callWithCatch(() =>
this.db.dbDrizzle.select().from(TasksSchema).where(eq(TasksSchema.parentId, taskId))
this.db.dbDrizzle.select().from(TasksSchema).where(eq(TasksSchema.parentId, taskId)).orderBy(asc(TasksSchema.id))
);
return result ?? [];
@@ -560,8 +560,10 @@ export class TasksRepository {
}
// complete
if (data.showCompleted === 0) conditions.push(eq(TasksSchema.complete, false));
else if (data.showCompleted === 1) conditions.push(eq(TasksSchema.complete, true));
if (!data.ignoreCompleted) {
if (data.showCompleted === 0) conditions.push(eq(TasksSchema.complete, false));
else if (data.showCompleted === 1) conditions.push(eq(TasksSchema.complete, true));
}
// assignee filter — через EXISTS
if (data.filters?.selectedUser) {
@@ -652,7 +654,7 @@ export class TasksRepository {
}
const result = await callWithCatch(() =>
this.db.dbDrizzle.select().from(TasksSchema).where(and(...conditions)).orderBy(asc(TasksSchema.kanbanOrder)).limit(10)
this.db.dbDrizzle.select().from(TasksSchema).where(and(...conditions)).orderBy(asc(TasksSchema.kanbanOrder)).limit(20)
);
return result ?? [];
}
@@ -12,7 +12,7 @@ export const TaskArkTypeUpdate = type({
'parentId?': 'number|null',
'description?': 'string',
'complete?': 'boolean',
'goalListId?': 'number',
'goalListId?': 'number|null',
// 'creatorId?': 'number',
'note?': 'string',
'priorityId?': '1|2|3|null',
@@ -23,7 +23,7 @@ export const TaskArkTypeUpdate = type({
'statusId?': 'number|null',
'taskOrder?': 'number',
'kanbanOrder?': 'number',
'amount?': 'number|null',
'amount?': 'string|null',
'transactionType?': '1|0|null',
'nodeGraphPosition?': 'object|null',
});
@@ -64,6 +64,7 @@ export const TaskArkTypeFetchTasksNew = type({
})
.pipe(TaskArkTypeFetchTasksNewFilters),
'unlimited?': type('string').pipe((v) => v === 'true'),
'ignoreCompleted?': type('string').pipe((v) => v === 'true'),
});
export type TaskArgFetchTasksNew = typeof TaskArkTypeFetchTasksNew.infer;
+4 -4
View File
@@ -27,7 +27,7 @@ export default defineConfig({
ecma: 2015,
compress: true,
mangle: {
properties: false,//couse error if true after build
properties: false,//cause error if true after build
},
format: {
comments: true,
@@ -37,9 +37,9 @@ export default defineConfig({
external: ['pg-native'],
output: {
format: 'umd',
entryFileNames: 'taskview-server-docker.js',
chunkFileNames: 'taskview-server-docker-[hash].js',
assetFileNames: 'taskview-server-docker-[name]-[hash].[ext]',
entryFileNames: 'taskview-server.js',
chunkFileNames: 'taskview-server-[hash].js',
assetFileNames: 'taskview-server-[name]-[hash].[ext]',
},
},
},
+30 -4
View File
@@ -1,19 +1,45 @@
#!/bin/bash
# This script builds the docker images for the taskview-ce-monorepo
# This script builds the docker images for TaskView Community Edition
# - taskview-ce-api-server
# - taskview-ce-db-migration
# - taskview-ce-webapp
set -e
VERSION=$1
# If version not provided, read from package.json
if [ -z "$VERSION" ]; then
VERSION=$(node -p "require('./package.json').version")
fi
echo $VERSION
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm use 24
node -v
echo "Building TaskView CE version: $VERSION"
# Build CE API
echo "========================================="
echo "Building CE API Server..."
echo "========================================="
cd api
bash build-docker-api.sh $VERSION
cd ..
# Build CE Web
echo "========================================="
echo "Building CE Web App..."
echo "========================================="
cd web
bash build-docker-web.sh $VERSION
cd ..
cd ..
echo "========================================="
echo "Build complete!"
echo "Images built:"
echo " - gimanhead/taskview-ce-api-server:$VERSION"
echo " - gimanhead/taskview-ce-webapp:$VERSION"
echo "========================================="
+41 -35
View File
@@ -1,36 +1,42 @@
{
"name": "taskview-ce-monorepo",
"version": "1.18.0",
"private": true,
"description": "TaskView CE monorepo containing web, API, and packages",
"workspaces": [
"web",
"api",
"taskview-packages/*"
],
"scripts": {
"dev:webapp": "cd web && pnpm dev",
"build:webapp": "cd web && pnpm build",
"build:webapp:dev": "cd web && pnpm build:dev",
"build:packages": "pnpm --filter=taskview-api --filter=taskview-db-schemas run build",
"dev:api": "cd api && pnpm dev",
"build:api": "cd api && pnpm build",
"install:all": "pnpm install",
"update:all": "pnpm update -r",
"clean": "pnpm store prune && rm -rf node_modules",
"lint": "pnpm -r run lint",
"test": "pnpm -r run test"
},
"devDependencies": {
"@types/node": "^18.19.119",
"typescript": "^5.7.3"
},
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.0.0"
},
"dependencies": {
"arktype": "2.1.20",
"drizzle-arktype": "0.1.3"
}
}
"name": "taskview-ce-monorepo",
"version": "1.20.4",
"private": true,
"description": "TaskView CE monorepo containing web, API, and packages",
"workspaces": [
"web",
"api",
"taskview-packages/*"
],
"scripts": {
"dev:webapp": "cd web && pnpm dev",
"build:webapp": "cd web && pnpm build",
"build:webapp:dev": "cd web && pnpm build:dev",
"build:packages": "pnpm --filter=taskview-api --filter=taskview-db-schemas run build",
"dev:api": "cd api && pnpm dev",
"build:api": "cd api && pnpm build",
"install:all": "pnpm install",
"update:all": "pnpm update -r",
"clean": "pnpm store prune && rm -rf node_modules",
"lint": "pnpm -r run lint",
"test": "pnpm -r run test"
},
"devDependencies": {
"@types/node": "^18.19.119",
"typescript": "^5.7.3"
},
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.0.0"
},
"dependencies": {
"@types/passport": "^1.0.17",
"@types/passport-github2": "^1.2.9",
"@types/passport-google-oauth20": "^2.0.17",
"arktype": "2.1.20",
"drizzle-arktype": "0.1.3",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0"
}
}
+5754 -6331
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "taskview-ce-api",
"name": "taskview-api",
"private": false,
"version": "1.17.0",
"type": "module",
@@ -2,7 +2,6 @@ export const TaskIncomeType = 1;
export const TaskExpenseType = 0;
export type TaskTransactionTypes = typeof TaskIncomeType | typeof TaskExpenseType | null;
//TODO add optional type for fields that can be not allowed by permissions (subtasks, note)
export interface TaskBase {
id: number;
description: string;
@@ -26,8 +25,8 @@ export interface TaskBase {
statusId: number | null;
taskOrder: number | null;
kanbanOrder: number | null;
//we add amount as number but got as string from API
amount: number | string | null; //string bec JS not working with numbers like 3.43
amount: number | string | null;
transactionType: TaskTransactionTypes;
nodeGraphPosition: Record<'x' | 'y', number> | null;
creatorId: number | null;
@@ -36,7 +35,7 @@ export interface TaskBase {
export interface Task extends TaskBase {
tags: number[];
historyId: null | number;
assignedUsers: number[]; //TODO how we update assigned users? maybe we need to do it in different module
assignedUsers: number[];
};
// we can not update defined fields
@@ -47,8 +46,7 @@ export type TaskResponseUpdate = Task | null;
export type TaskArgAdd = Pick<Task, 'goalId' | 'description'>
& Partial<Omit<Task, NotAllowedToUpdate | 'subtasks' | 'tags' | 'assignedUsers' | 'creatorId' | 'amount'>>
//we add amount as number but got as string from API
& { amount?: number | null };
& { amount?: number | string | null };
export type TaskResponseAdd = Task | null;
@@ -77,6 +75,7 @@ export type TaskArgFetch = {
searchText?: string;
filters?: TaskFilters;
unlimited?: boolean;
ignoreCompleted?: boolean;
};
export type TaskResponseFetch = Task[];
-1
View File
@@ -7,7 +7,6 @@ import TvGoalListApi from "./api/goals-list";
import TvTagsApi from "./api/tags";
import TvKanban from "./api/kanban";
// const TASKVIEW_URL = 'https://apitaskview.handscream.com/';
export class TvApi {
protected $axios: AxiosInstance;
@@ -1,5 +1,5 @@
{
"name": "taskview-ce-db-schemas",
"name": "taskview-db-schemas",
"version": "1.17.0",
"type": "module",
"main": "./dist/taskview-db-schemas.umd.js",
@@ -1,4 +1,4 @@
import { integer, pgSchema, varchar, boolean, date, time, jsonb } from "drizzle-orm/pg-core";
import { integer, numeric, pgSchema, varchar, boolean, date, time, jsonb } from "drizzle-orm/pg-core";
import { createInsertSchema } from 'drizzle-arktype';
import { UsersSchema } from "./users.schema";
@@ -20,7 +20,7 @@ export const TasksSchema = pgSchema('tasks').table('tasks', {
statusId: integer('status_id'), //kanban column id
taskOrder: integer('task_order'),
kanbanOrder: integer('kanban_order'),
amount: integer(),
amount: numeric({ precision: 10, scale: 2 }),
transactionType: integer('transaction_type').$type<1 | 0 | null>(),
nodeGraphPosition: jsonb('node_graph_position'),
});
+5
View File
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"context7@claude-plugins-official": true
}
}
+13
View File
@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
+37
View File
@@ -0,0 +1,37 @@
name: ci
on: push
jobs:
ci:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
node: [22]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install node
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm run lint
- name: Build
run: pnpm run build
- name: Typecheck
run: pnpm run typecheck
+38 -2
View File
@@ -1,2 +1,38 @@
!build-upload.sh
tsconfig.tsbuildinfo
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Auto-generated type declarations
auto-imports.d.ts
components.d.ts
tsconfig.app.tsbuildinfo
tsconfig.node.tsbuildinfo
# Environment variables
.env
Info.plist
e2e_logs
e2e_pgdata
e2e_updates
playwright-report
+2 -2
View File
@@ -54,8 +54,8 @@ captures/
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
*.jks
*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
+5 -8
View File
@@ -1,28 +1,25 @@
apply plugin: 'com.android.application'
android {
namespace "com.handscream.taskview.app"
compileSdk rootProject.ext.compileSdkVersion
namespace = "com.handscream.taskview.app"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.handscreamgnl.taskview.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1150
versionName "1.15.0"
versionCode 1201
versionName "1.20.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
setProperty("archivesBaseName", "TaskView-$versionName")
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
}
}
+1
View File
@@ -10,6 +10,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-browser')
implementation project(':capacitor-device')
implementation project(':capacitor-preferences')
implementation project(':capacitor-splash-screen')
+11 -4
View File
@@ -1,23 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
@@ -25,11 +30,13 @@
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
</manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
+2 -2
View File
@@ -7,8 +7,8 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.3'
classpath 'com.google.gms:google-services:4.4.2'
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
+9 -6
View File
@@ -1,18 +1,21 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../../node_modules/.pnpm/@capacitor+android@7.4.4_@capacitor+core@7.4.4/node_modules/@capacitor/android/capacitor')
project(':capacitor-android').projectDir = new File('../../../node_modules/.pnpm/@capacitor+android@8.1.0_@capacitor+core@8.1.0/node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/app/android')
project(':capacitor-app').projectDir = new File('../../../node_modules/.pnpm/@capacitor+app@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/app/android')
include ':capacitor-browser'
project(':capacitor-browser').projectDir = new File('../../../node_modules/.pnpm/@capacitor+browser@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/browser/android')
include ':capacitor-device'
project(':capacitor-device').projectDir = new File('../../node_modules/.pnpm/@capacitor+device@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/device/android')
project(':capacitor-device').projectDir = new File('../../../node_modules/.pnpm/@capacitor+device@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/device/android')
include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../../node_modules/.pnpm/@capacitor+preferences@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/preferences/android')
project(':capacitor-preferences').projectDir = new File('../../../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/preferences/android')
include ':capacitor-splash-screen'
project(':capacitor-splash-screen').projectDir = new File('../../node_modules/.pnpm/@capacitor+splash-screen@7.0.3_@capacitor+core@7.4.4/node_modules/@capacitor/splash-screen/android')
project(':capacitor-splash-screen').projectDir = new File('../../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/splash-screen/android')
include ':capgo-capacitor-updater'
project(':capgo-capacitor-updater').projectDir = new File('../../node_modules/.pnpm/@capgo+capacitor-updater@7.29.0_@capacitor+core@7.4.4/node_modules/@capgo/capacitor-updater/android')
project(':capgo-capacitor-updater').projectDir = new File('../../../node_modules/.pnpm/@capgo+capacitor-updater@8.43.2_@capacitor+core@8.1.0/node_modules/@capgo/capacitor-updater/android')
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+14 -11
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,7 +85,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -111,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -144,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -201,16 +204,16 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
+94 -92
View File
@@ -1,92 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+13 -13
View File
@@ -1,16 +1,16 @@
ext {
minSdkVersion = 23
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.12.1'
minSdkVersion = 24
compileSdkVersion = 36
targetSdkVersion = 36
androidxActivityVersion = '1.11.0'
androidxAppCompatVersion = '1.7.1'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.17.0'
androidxFragmentVersion = '1.8.9'
coreSplashScreenVersion = '1.2.0'
androidxWebkitVersion = '1.14.0'
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
androidxJunitVersion = '1.3.0'
androidxEspressoCoreVersion = '3.7.0'
cordovaAndroidVersion = '14.0.1'
}
+24 -22
View File
@@ -1,29 +1,31 @@
import type { CapacitorConfig } from '@capacitor/cli';
import type { CapacitorConfig } from '@capacitor/cli'
const config: CapacitorConfig = {
appId: 'com.handscream.taskview.app',
appName: 'TaskView',
webDir: 'dist',
appId: 'com.handscream.taskview.app',
appName: 'TaskView',
webDir: 'dist',
zoomEnabled: false,
android: {
zoomEnabled: false,
android: {
zoomEnabled: false,
},
ios: {
zoomEnabled: false,
webContentsDebuggingEnabled: true,
},
server: {
hostname: 'app.taskview.tech',
},
plugins: {
CapacitorUpdater: {
autoUpdate: false,
},
ios: {
zoomEnabled: false,
CapacitorCookies: {
enabled: true,
},
server: {
hostname: 'taskview.handscream.com',
//androidScheme: 'https://',
CapacitorHttp: {
enabled: true,
},
plugins: {
"CapacitorUpdater": {
"autoUpdate": false,
}
},
// server: {
// url: 'http://192.168.0.2:3000',
// cleartext: true,
// },
};
},
}
export default config;
export default config
+29
View File
@@ -0,0 +1,29 @@
import { test, expect } from '@playwright/test'
import { TEST_USER } from './fixtures/auth'
test.describe('Login', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('shows login page with welcome message', async ({ page }) => {
await expect(page.getByRole('heading', { level: 1 })).toContainText(/welcome|возвращением/i)
})
test('user can login with password and reach dashboard', async ({ page }) => {
// Switch to Password tab (default is Magic link)
await page.getByRole('tab', { name: /password|пароль/i }).click()
// Fill credentials
await page.getByTestId('login-input').fill(TEST_USER.login)
await page.getByTestId('password-input').fill(TEST_USER.password)
// Submit
await page.getByTestId('sign-in-button').click()
// Should redirect to user dashboard
await expect(page).toHaveURL(/\/user/)
// Dashboard should be visible (sidebar or main content)
await expect(page.locator('body')).not.toContainText(/invalid|неверный|error|ошибка/i)
})
})
+51
View File
@@ -0,0 +1,51 @@
# E2E test stack: PostgreSQL + migration + API
# Run from repo root: bash build-dockers.sh (builds images first)
# Then: docker compose -f se/web-nuxt-ui/e2e/docker-compose.yml up -d
#
# API is exposed on 1401 so web-nuxt-ui (useTaskViewMainUrl) can connect.
services:
db:
image: postgres:17
restart: unless-stopped
env_file:
- ../../dockers-check/.env.postgresql
volumes:
- ./e2e_pgdata:/var/lib/postgresql/data
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tvdbuser -d taskviewdb"]
interval: 5s
timeout: 5s
retries: 5
migration:
image: gimanhead/taskview-se-db-migration:latest
restart: "no"
depends_on:
db:
condition: service_healthy
env_file:
- ../../dockers-check/.env.taskview
api:
image: gimanhead/taskview-se-api-server:latest
restart: "no"
ports:
- "1401:1401"
depends_on:
db:
condition: service_healthy
migration:
condition: service_completed_successfully
env_file:
- ../../dockers-check/.env.taskview
volumes:
- ./e2e_logs:/usr/src/app/logs
- ./e2e_updates:/usr/src/app/updates
volumes:
e2e_pgdata:
e2e_logs:
e2e_updates:
+8
View File
@@ -0,0 +1,8 @@
/**
* E2E auth fixtures. Credentials match DB seed from community migrations
* (user / user1!#Q from app_permissions.sql).
*/
export const TEST_USER = {
login: 'user',
password: 'user1!#Q',
}
+25
View File
@@ -0,0 +1,25 @@
import { execSync } from 'node:child_process'
import { readFileSync, existsSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
function loadEnv(dir: string) {
const envPath = path.join(dir, '.env')
if (!existsSync(envPath)) return
for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
const m = line.match(/^([^#=]+)=(.*)$/)
if (m) process.env[m[1].trim()] = m[2].trim()
}
}
export default async function globalSetup() {
const e2eDir = path.dirname(fileURLToPath(import.meta.url))
const projectDir = path.join(e2eDir, '..')
loadEnv(projectDir)
const setupScript = path.join(e2eDir, 'docker-setup.sh')
execSync(`bash "${setupScript}"`, {
stdio: 'inherit',
cwd: path.join(e2eDir, '../../..'),
env: process.env,
})
}
+16
View File
@@ -0,0 +1,16 @@
import { execSync } from 'node:child_process'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
export default async function globalTeardown() {
const e2eDir = path.dirname(fileURLToPath(import.meta.url))
const teardownScript = path.join(e2eDir, 'docker-teardown.sh')
try {
execSync(`bash "${teardownScript}"`, {
stdio: 'inherit',
cwd: path.join(e2eDir, '../../..'),
})
} catch {
console.warn('[e2e] Teardown: some containers may have already been stopped')
}
}
+71
View File
@@ -0,0 +1,71 @@
import { test, expect } from '@playwright/test'
import { login } from './test-helpers'
async function addProject(page: import('@playwright/test').Page, name: string) {
const input = page.getByTestId('project-add-input').first()
await input.waitFor({ state: 'visible', timeout: 15000 })
await input.fill(name)
await input.press('Enter')
await expect(page.getByText(name).first()).toBeVisible({ timeout: 10000 })
}
async function openProjectMenu(page: import('@playwright/test').Page, projectName: string) {
const row = page.getByTestId(`project-row-${projectName}`)
await row.first().waitFor({ state: 'visible', timeout: 10000 })
await row.first().getByTestId('project-menu-trigger').click()
}
test.describe('Projects', () => {
test.setTimeout(15_000)
test.beforeEach(async ({ page }) => {
await page.goto('/')
await login(page)
})
test('user can add project', async ({ page }) => {
const projectName = `Test Project ${Date.now()}`
await addProject(page, projectName)
})
test('user can edit project', async ({ page }) => {
const projectName = `Edit Me ${Date.now()}`
await addProject(page, projectName)
const newName = `Edited ${Date.now()}`
await openProjectMenu(page, projectName)
await page.getByTestId('context-menu-edit').click()
await page.getByTestId('project-edit-name').fill(newName)
await page.getByTestId('project-edit-save').click()
await expect(page.getByText(newName).first()).toBeVisible({ timeout: 5000 })
})
test('user can delete project', async ({ page }) => {
const projectName = `Delete Me ${Date.now()}`
await addProject(page, projectName)
await openProjectMenu(page, projectName)
await page.getByTestId('context-menu-delete').click()
await page.getByRole('button', { name: /^yes|^да$/i }).click()
await expect(page.getByText(projectName)).toHaveCount(0)
})
test('user can archive project', async ({ page }) => {
const projectName = `Archive Me ${Date.now()}`
await addProject(page, projectName)
await openProjectMenu(page, projectName)
await page.getByTestId('context-menu-move-to-archive').click()
await page.getByTestId('archive-list-collapsible').click()
await expect(page.getByTestId(`project-row-${projectName}`)).toBeVisible()
})
test('user can unarchive project', async ({ page }) => {
const projectName = `Unarchive Me ${Date.now()}`
await addProject(page, projectName)
await openProjectMenu(page, projectName)
await page.getByTestId('context-menu-move-to-archive').click()
await page.getByTestId('archive-list-collapsible').click()
await openProjectMenu(page, projectName)
await page.getByTestId('context-menu-restore-from-archive').click({ force: true })
await expect(page.getByText(projectName).first()).toBeVisible({ timeout: 5000 })
})
})
+8
View File
@@ -0,0 +1,8 @@
import { TEST_USER } from './fixtures/auth'
export async function login(page: Page) {
await page.getByRole('tab', { name: /password|пароль/i }).click()
await page.getByTestId('login-input').fill(TEST_USER.login)
await page.getByTestId('password-input').fill(TEST_USER.password)
await page.getByTestId('sign-in-button').click()
}
+103
View File
@@ -0,0 +1,103 @@
import js from '@eslint/js'
import eslintPluginVue from 'eslint-plugin-vue'
import ts from 'typescript-eslint'
import { fileURLToPath } from 'node:url'
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...eslintPluginVue.configs['flat/recommended'],
{
files: ['**/*.ts', '**/*.js'],
languageOptions: {
parserOptions: {
tsconfigRootDir: fileURLToPath(new URL('.', import.meta.url)),
},
},
},
{
files: ['*.vue', '**/*.vue'],
languageOptions: {
parserOptions: {
parser: '@typescript-eslint/parser',
tsconfigRootDir: fileURLToPath(new URL('.', import.meta.url)),
},
},
rules: {
'vue/multi-word-component-names': 'off',
'no-undef': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
// Formatting rules
'vue/script-indent': ['error', 2, { baseIndent: 0 }],
'vue/html-indent': ['error', 2],
'vue/max-attributes-per-line': [
'error',
{
singleline: 1,
multiline: 1,
},
],
'vue/first-attribute-linebreak': [
'error',
{
singleline: 'ignore',
multiline: 'below',
},
],
'vue/html-closing-bracket-newline': [
'error',
{
singleline: 'never',
multiline: 'always',
},
],
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'always',
component: 'always',
},
svg: 'always',
math: 'always',
},
],
'vue/attribute-hyphenation': ['error', 'always'],
'vue/v-bind-style': ['error', 'shorthand'],
'vue/v-on-style': ['error', 'shorthand'],
'vue/v-slot-style': ['error', 'shorthand'],
},
},
{
files: ['**/*.ts', '**/*.js', '**/*.vue'],
rules: {
semi: ['error', 'never'],
quotes: ['error', 'single'],
'comma-dangle': ['error', 'always-multiline'],
'keyword-spacing': ['error', { before: true, after: true }],
'space-before-blocks': ['error', 'always'],
'space-before-function-paren': ['error', { anonymous: 'always', named: 'never', asyncArrow: 'always' }],
'space-infix-ops': 'error',
'space-in-parens': ['error', 'never'],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': ['error', 'never'],
'comma-spacing': ['error', { before: false, after: true }],
'key-spacing': ['error', { beforeColon: false, afterColon: true }],
'arrow-spacing': ['error', { before: true, after: true }],
},
},
{
files: ['**/*.ts', '**/*.js'],
rules: {
indent: ['error', 2],
},
},
{
files: ['**/*.vue'],
rules: {
indent: 'off',
},
},
)
+25 -12
View File
@@ -1,14 +1,27 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/tv-logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Task View</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<link rel="icon"
type="image/svg+xml"
href="/logo.svg" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="preconnect"
href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=public-sans:400,500,600,700"
rel="stylesheet" />
<title>TaskView</title>
<meta name="description"
content="TaskView is a self-hosted project and task management platform focused on clarity, ownership, and control.">
</head>
<body>
<div id="app"
class="isolate"></div>
<script type="module"
src="/src/main.ts"></script>
</body>
</html>
+56 -98
View File
@@ -3,37 +3,31 @@
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 4D22ABE82AF431CB00220026 /* CapApp-SPM */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
A75AC5A184BA226F87D9DFB4 /* Pods_TaskView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33826060E12180E2E6AE45B /* Pods_TaskView.framework */; };
AA2DDEAF2BD5EE2800137350 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AA2DDEAE2BD5EE2800137350 /* PrivacyInfo.xcprivacy */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
3DDB5660BE7C0283C0A61E6B /* Pods-TaskView.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TaskView.release.xcconfig"; path = "Pods/Target Support Files/Pods-TaskView/Pods-TaskView.release.xcconfig"; sourceTree = "<group>"; };
4ED8FB2BBB1F064473F2BF99 /* Pods-TaskView.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TaskView.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TaskView/Pods-TaskView.debug.xcconfig"; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* TaskView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TaskView.app; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
AA2DDEAE2BD5EE2800137350 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
C33826060E12180E2E6AE45B /* Pods_TaskView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TaskView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -41,36 +35,26 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A75AC5A184BA226F87D9DFB4 /* Pods_TaskView.framework in Frameworks */,
4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup;
children = (
C33826060E12180E2E6AE45B /* Pods_TaskView.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
504EC2FB1FED79650016851F = {
isa = PBXGroup;
children = (
AA2DDEAE2BD5EE2800137350 /* PrivacyInfo.xcprivacy */,
958DCC722DB07C7200EA8C5F /* debug.xcconfig */,
504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
);
sourceTree = "<group>";
};
504EC3051FED79650016851F /* Products */ = {
isa = PBXGroup;
children = (
504EC3041FED79650016851F /* TaskView.app */,
504EC3041FED79650016851F /* App.app */,
);
name = Products;
sourceTree = "<group>";
@@ -90,37 +74,27 @@
path = App;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
4ED8FB2BBB1F064473F2BF99 /* Pods-TaskView.debug.xcconfig */,
3DDB5660BE7C0283C0A61E6B /* Pods-TaskView.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
504EC3031FED79650016851F /* TaskView */ = {
504EC3031FED79650016851F /* App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "TaskView" */;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = TaskView;
name = App;
packageProductDependencies = (
4D22ABE82AF431CB00220026 /* CapApp-SPM */,
);
productName = App;
productReference = 504EC3041FED79650016851F /* TaskView.app */;
productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -129,8 +103,8 @@
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 920;
LastUpgradeCheck = 920;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
@@ -148,11 +122,14 @@
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
packageReferences = (
D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */,
);
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
504EC3031FED79650016851F /* TaskView */,
504EC3031FED79650016851F /* App */,
);
};
/* End PBXProject section */
@@ -163,7 +140,6 @@
buildActionMask = 2147483647;
files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
AA2DDEAF2BD5EE2800137350 /* PrivacyInfo.xcprivacy in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
@@ -174,42 +150,6 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-TaskView-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-TaskView/Pods-TaskView-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
504EC3001FED79650016851F /* Sources */ = {
isa = PBXSourcesBuildPhase;
@@ -243,6 +183,7 @@
/* Begin XCBuildConfiguration section */
504EC3141FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@@ -289,7 +230,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -340,28 +281,30 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 4ED8FB2BBB1F064473F2BF99 /* Pods-TaskView.debug.xcconfig */;
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1.15.0;
CURRENT_PROJECT_VERSION = 1.20.1;
DEVELOPMENT_TEAM = H2W2SG48JT;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TaskView;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.15.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.20.1;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.handscream.taskview.app;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -373,18 +316,18 @@
};
504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3DDB5660BE7C0283C0A61E6B /* Pods-TaskView.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1.15.0;
CURRENT_PROJECT_VERSION = 1.20.1;
DEVELOPMENT_TEAM = H2W2SG48JT;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TaskView;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.15.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.20.1;
PRODUCT_BUNDLE_IDENTIFIER = com.handscream.taskview.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -405,7 +348,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "TaskView" */ = {
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3171FED79650016851F /* Debug */,
@@ -415,6 +358,21 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = "CapApp-SPM";
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
4D22ABE82AF431CB00220026 /* CapApp-SPM */ = {
isa = XCSwiftPackageProductDependency;
package = D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */;
productName = "CapApp-SPM";
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}
@@ -0,0 +1,51 @@
{
"originHash" : "e5bfee72a3975177edbeb9593e2d4a7624097d5319fbcc2894685c2c76545af8",
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "3f99050e75bbc6fe71fc323adabb039756680016",
"version" : "5.11.1"
}
},
{
"identity" : "bigint",
"kind" : "remoteSourceControl",
"location" : "https://github.com/attaswift/BigInt.git",
"state" : {
"revision" : "e07e00fa1fd435143a2dcf8b7eec9a7710b2fdfe",
"version" : "5.7.0"
}
},
{
"identity" : "capacitor-swift-pm",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ionic-team/capacitor-swift-pm.git",
"state" : {
"revision" : "1f324fe7bdbdc8c5cc28bea48a231545a8c2e823",
"version" : "8.1.0"
}
},
{
"identity" : "version",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mrackwitz/Version.git",
"state" : {
"revision" : "fd4b0eb5756aa7f1c33977fb626cf37d2140a3a0",
"version" : "0.8.0"
}
},
{
"identity" : "zipfoundation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/weichsel/ZIPFoundation.git",
"state" : {
"revision" : "22787ffb59de99e5dc1fbfe80b19c97a904ad48d",
"version" : "0.9.20"
}
}
],
"version" : 3
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17132" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17105"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -24,7 +24,7 @@
</scene>
</scenes>
<resources>
<image name="Splash" width="1639" height="1639"/>
<image name="Splash" width="1366" height="1366"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
+9
View File
@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
+37
View File
@@ -0,0 +1,37 @@
// swift-tools-version: 5.9
import PackageDescription
// DO NOT MODIFY THIS FILE - managed by Capacitor CLI commands
let package = Package(
name: "CapApp-SPM",
platforms: [.iOS(.v15)],
products: [
.library(
name: "CapApp-SPM",
targets: ["CapApp-SPM"])
],
dependencies: [
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.1.0"),
.package(name: "CapacitorApp", path: "../../../../node_modules/.pnpm/@capacitor+app@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/app"),
.package(name: "CapacitorBrowser", path: "../../../../node_modules/.pnpm/@capacitor+browser@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/browser"),
.package(name: "CapacitorDevice", path: "../../../../node_modules/.pnpm/@capacitor+device@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/device"),
.package(name: "CapacitorPreferences", path: "../../../../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/preferences"),
.package(name: "CapacitorSplashScreen", path: "../../../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.1.0/node_modules/@capacitor/splash-screen"),
.package(name: "CapgoCapacitorUpdater", path: "../../../../node_modules/.pnpm/@capgo+capacitor-updater@8.43.8_@capacitor+core@8.1.0/node_modules/@capgo/capacitor-updater")
],
targets: [
.target(
name: "CapApp-SPM",
dependencies: [
.product(name: "Capacitor", package: "capacitor-swift-pm"),
.product(name: "Cordova", package: "capacitor-swift-pm"),
.product(name: "CapacitorApp", package: "CapacitorApp"),
.product(name: "CapacitorBrowser", package: "CapacitorBrowser"),
.product(name: "CapacitorDevice", package: "CapacitorDevice"),
.product(name: "CapacitorPreferences", package: "CapacitorPreferences"),
.product(name: "CapacitorSplashScreen", package: "CapacitorSplashScreen"),
.product(name: "CapgoCapacitorUpdater", package: "CapgoCapacitorUpdater")
]
)
]
)
+5
View File
@@ -0,0 +1,5 @@
# CapApp-SPM
This SPM is used to host SPM dependencies for you Capacitor project
Do not modify the contents of it or there may be unintended consequences.
@@ -0,0 +1 @@
public let isCapacitorApp = true
-28
View File
@@ -1,28 +0,0 @@
require_relative '../../../node_modules/.pnpm/@capacitor+ios@7.4.4_@capacitor+core@7.4.4/node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '14.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../../node_modules/.pnpm/@capacitor+ios@7.4.4_@capacitor+core@7.4.4/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../../node_modules/.pnpm/@capacitor+ios@7.4.4_@capacitor+core@7.4.4/node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/app'
pod 'CapacitorDevice', :path => '../../../node_modules/.pnpm/@capacitor+device@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/device'
pod 'CapacitorPreferences', :path => '../../../node_modules/.pnpm/@capacitor+preferences@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/preferences'
pod 'CapacitorSplashScreen', :path => '../../../node_modules/.pnpm/@capacitor+splash-screen@7.0.3_@capacitor+core@7.4.4/node_modules/@capacitor/splash-screen'
pod 'CapgoCapacitorUpdater', :path => '../../../node_modules/.pnpm/@capgo+capacitor-updater@7.29.0_@capacitor+core@7.4.4/node_modules/@capgo/capacitor-updater'
end
target 'TaskView' do
capacitor_pods
# Add your Pods here
end
post_install do |installer|
assertDeploymentTarget(installer)
end
-71
View File
@@ -1,71 +0,0 @@
PODS:
- Alamofire (5.10.2)
- BigInt (5.2.0)
- Capacitor (7.4.4):
- CapacitorCordova
- CapacitorApp (7.0.1):
- Capacitor
- CapacitorCordova (7.4.4)
- CapacitorDevice (7.0.1):
- Capacitor
- CapacitorPreferences (7.0.1):
- Capacitor
- CapacitorSplashScreen (7.0.3):
- Capacitor
- CapgoCapacitorUpdater (7.29.0):
- Alamofire (= 5.10.2)
- BigInt (= 5.2.0)
- Capacitor
- SSZipArchive (= 2.4.3)
- Version (= 0.8.0)
- SSZipArchive (2.4.3)
- Version (0.8.0)
DEPENDENCIES:
- "Capacitor (from `../../../node_modules/.pnpm/@capacitor+ios@7.4.4_@capacitor+core@7.4.4/node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/app`)"
- "CapacitorCordova (from `../../../node_modules/.pnpm/@capacitor+ios@7.4.4_@capacitor+core@7.4.4/node_modules/@capacitor/ios`)"
- "CapacitorDevice (from `../../../node_modules/.pnpm/@capacitor+device@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/device`)"
- "CapacitorPreferences (from `../../../node_modules/.pnpm/@capacitor+preferences@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/preferences`)"
- "CapacitorSplashScreen (from `../../../node_modules/.pnpm/@capacitor+splash-screen@7.0.3_@capacitor+core@7.4.4/node_modules/@capacitor/splash-screen`)"
- "CapgoCapacitorUpdater (from `../../../node_modules/.pnpm/@capgo+capacitor-updater@7.29.0_@capacitor+core@7.4.4/node_modules/@capgo/capacitor-updater`)"
SPEC REPOS:
trunk:
- Alamofire
- BigInt
- SSZipArchive
- Version
EXTERNAL SOURCES:
Capacitor:
:path: "../../../node_modules/.pnpm/@capacitor+ios@7.4.4_@capacitor+core@7.4.4/node_modules/@capacitor/ios"
CapacitorApp:
:path: "../../../node_modules/.pnpm/@capacitor+app@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/app"
CapacitorCordova:
:path: "../../../node_modules/.pnpm/@capacitor+ios@7.4.4_@capacitor+core@7.4.4/node_modules/@capacitor/ios"
CapacitorDevice:
:path: "../../../node_modules/.pnpm/@capacitor+device@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/device"
CapacitorPreferences:
:path: "../../../node_modules/.pnpm/@capacitor+preferences@7.0.1_@capacitor+core@7.4.4/node_modules/@capacitor/preferences"
CapacitorSplashScreen:
:path: "../../../node_modules/.pnpm/@capacitor+splash-screen@7.0.3_@capacitor+core@7.4.4/node_modules/@capacitor/splash-screen"
CapgoCapacitorUpdater:
:path: "../../../node_modules/.pnpm/@capgo+capacitor-updater@7.29.0_@capacitor+core@7.4.4/node_modules/@capgo/capacitor-updater"
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
BigInt: f668a80089607f521586bbe29513d708491ef2f7
Capacitor: 09d9ff8e9618e8c4b3cab2bbee34a17215dd2fef
CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39
CapacitorCordova: bf648a636f3c153f652d312ae145fb508b6ffced
CapacitorDevice: fe3f190e1d718f4607bdc6b73993433d1c84f409
CapacitorPreferences: cbf154e5e5519b7f5ab33817a334dda1e98387f9
CapacitorSplashScreen: d06ae8804808e9f649a08e7bb7f283c77b688084
CapgoCapacitorUpdater: b3a419b281c01eb9efc8969fe5183ea0da636d25
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
Version: de5907f2c5d0f3cf21708db7801d1d5401139486
PODFILE CHECKSUM: 65fabdcdf017e31d75062b30247956106ea17004
COCOAPODS: 1.16.2
+1
View File
@@ -0,0 +1 @@
CAPACITOR_DEBUG = true
+68 -78
View File
@@ -1,95 +1,85 @@
{
"name": "taskview-ce-webapp",
"version": "1.17.0",
"name": "web-nuxt-ui",
"private": true,
"type": "module",
"version": "1.20.4",
"scripts": {
"dev": "vite",
"build:dev": "BRANCH_MODE=dev pnpm run build",
"build": "run-p type-check build-only",
"preview": "vite preview",
"test:unit": "vitest --environment jsdom --root src/",
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
"build": "pnpm run typecheck && vite build",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit --project tsconfig.json",
"lint": "eslint . --config eslint.config.mjs --fix",
"lint:biome": "biome lint .",
"format": "biome format --write .",
"check": "biome check --write ."
"preview": "vite preview",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"format": "eslint src --fix",
"typecheck": "vue-tsc -p ./tsconfig.app.json",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed",
"test:e2e:install": "playwright install chromium",
"test:e2e:skip-build": "SKIP_BUILD=1 playwright test"
},
"dependencies": {
"@capacitor/android": "^7.0.0",
"@capacitor/app": "7.0.1",
"@capacitor/core": "^7.0.0",
"@capacitor/device": "7.0.1",
"@capacitor/ios": "^7.0.0",
"@capacitor/preferences": "7.0.1",
"@capacitor/splash-screen": "^7.0.1",
"@capgo/capacitor-updater": "^7.7.7",
"@capacitor/android": "^8.1.0",
"@capacitor/app": "8.0.1",
"@capacitor/browser": "^8.0.1",
"@capacitor/core": "^8.1.0",
"@capacitor/device": "8.0.1",
"@capacitor/ios": "^8.1.0",
"@capacitor/preferences": "8.0.1",
"@capacitor/splash-screen": "^8.0.1",
"@capgo/capacitor-updater": "^8.43.2",
"@dagrejs/dagre": "^1.1.5",
"@tiptap/extension-color": "^3.0.0",
"@tiptap/extension-link": "^3.0.0",
"@tiptap/extension-list-item": "^3.0.0",
"@tiptap/extension-placeholder": "^3.0.0",
"@tiptap/extension-text-style": "^3.0.0",
"@tiptap/pm": "^3.0.0",
"@tiptap/starter-kit": "^3.0.0",
"@tiptap/vue-3": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^8.24.1",
"@typescript-eslint/parser": "^8.24.1",
"@iconify-json/carbon": "^1.2.18",
"@iconify-json/lucide": "^1.2.90",
"@iconify-json/mdi": "^1.2.3",
"@iconify/vue": "^5.0.0",
"@internationalized/date": "^3.10.1",
"@nuxt/ui": "^4.4.0",
"@tanstack/table-core": "^8.21.3",
"@tiptap/core": "^3.20.0",
"@tiptap/extension-drag-handle-vue-3": "^3.20.0",
"@tiptap/extension-text-align": "^3.20.0",
"@tiptap/extension-underline": "^3.20.0",
"@tiptap/pm": "^3.20.0",
"@tiptap/starter-kit": "^3.20.0",
"@tiptap/vue-3": "^3.20.0",
"@types/qs": "^6.14.0",
"@unovis/ts": "^1.6.2",
"@unovis/vue": "^1.6.2",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.3",
"@vue-flow/core": "^1.46.4",
"@vue-flow/core": "^1.48.1",
"@vue-flow/minimap": "^1.5.4",
"@vue-flow/node-resizer": "^1.5.0",
"@vue-flow/node-toolbar": "^1.1.1",
"@vue/eslint-config-typescript": "^14.4.0",
"@vueuse/components": "^10.9.0",
"@vueuse/core": "^10.9.0",
"echarts": "^5.5.1",
"globals": "^15.15.0",
"motion-v": "^1.1.0-alpha.1",
"pinia": "^2.0.28",
"@vueuse/core": "^14.1.0",
"arktype": "2.1.20",
"axios": "^1.13.4",
"date-fns": "^4.1.0",
"pinia": "^2.3.1",
"qs": "^6.14.1",
"scule": "^1.3.0",
"tailwindcss": "^4.1.18",
"taskview-api": "workspace:^",
"vue": "^3.2.45",
"vue-router": "4.3.2",
"vuedraggable": "^4.1.0"
"vue": "^3.5.27",
"vue-i18n": "^11.2.8",
"vue-router": "^4.6.4",
"vuedraggable": "^4.1.0",
"zod": "^4.3.5"
},
"devDependencies": {
"@biomejs/biome": "^2.3.8",
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "^7.0.0",
"@date-io/date-fns": "^3.2.0",
"@mdi/js": "^7.1.96",
"@rushstack/eslint-patch": "^1.1.4",
"@types/jsdom": "^20.0.1",
"@types/node": "^18.19.119",
"@types/qs": "^6.9.7",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/test-utils": "^2.2.6",
"@vue/tsconfig": "0.7.0",
"autoprefixer": "^10.4.20",
"axios": "^1.2.3",
"check-password-strength": "^2.0.7",
"eslint": "9.20.1",
"eslint-plugin-vue": "^9.3.0",
"jsdom": "^20.0.3",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.49",
"prettier": "^2.7.1",
"qs": "^6.11.0",
"sass": "^1.69.7",
"sass-loader": "^13.3.3",
"start-server-and-test": "^1.15.2",
"tailwindcss": "^3.4.16",
"typescript": "^5.7.3",
"typescript-eslint": "^8.22.0",
"vite": "^4.0.0",
"vitest": "^0.25.6",
"vue-i18n": "11.1.10",
"vue-tsc": "3.0.3",
"vuetify": "3.7.9"
}
}
"@capacitor/cli": "^8.1.0",
"@playwright/test": "^1.49.0",
"@types/node": "^22.19.7",
"@vitejs/plugin-vue": "^6.0.3",
"eslint": "^9.39.2",
"eslint-plugin-vue": "^10.7.0",
"playwright": "^1.49.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.1",
"vite": "^7.3.1",
"vue-tsc": "^3.2.2"
},
"packageManager": "pnpm@10.28.1"
}
+28
View File
@@ -0,0 +1,28 @@
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
// globalSetup: './e2e/global-setup.ts',
// globalTeardown: './e2e/global-teardown.ts',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: process.env.CI
? undefined
: {
command: 'pnpm dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
timeout: 15_000,
expect: { timeout: 5_000 },
})

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+13
View File
@@ -0,0 +1,13 @@
{
"extends": [
"github>nuxt/renovate-config-nuxt"
],
"lockFileMaintenance": {
"enabled": true
},
"packageRules": [{
"matchDepTypes": ["resolutions"],
"enabled": false
}],
"postUpdateOptions": ["pnpmDedupe"]
}
+5 -45
View File
@@ -1,49 +1,9 @@
<template>
<component :is="layout">
<router-view />
</component>
<UApp>
<RouterView />
</UApp>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { useAdditionalServer } from '@/composition/useAdditionalServer';
import { isLoggedIn } from '@/helpers/app-helper';
export default defineComponent({
components: {
DefaultLayout: defineAsyncComponent(() => import('@/layout/DefaultLayout.vue')),
AdminLayout: defineAsyncComponent(() => import('@/layout/AdminLayout.vue')),
UserLayout: defineAsyncComponent(() => import('@/layout/UserLayout.vue')),
},
computed: {
layout() {
return isLoggedIn.value ? 'UserLayout' : 'DefaultLayout';
},
},
mounted() {
const handleViewportHeight = () => {
document.documentElement.style.setProperty('--viewport-height', `${window.innerHeight}px`);
console.log('viewport-height', window.innerHeight);
};
window.addEventListener('resize', handleViewportHeight);
handleViewportHeight();
this.blurWhenClickOutsideInput();
},
async created() {
const { mainServer, setMainServer } = await useAdditionalServer();
setMainServer(mainServer.value);
// console.log('mainServer', mainServer.value);
},
methods: {
//input blur for safari
blurWhenClickOutsideInput() {
if (document.body.firstElementChild) {
(document.body.firstElementChild as HTMLElement).tabIndex = 1;
}
},
},
});
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
+83
View File
@@ -0,0 +1,83 @@
@import "tailwindcss" theme(static);
@import "@nuxt/ui";
@theme static {
--font-sans: 'Public Sans', sans-serif;
}
@theme {
--tv-ui-bg-elevated: #fff;
--color-tv-ui-bg-elevated: var(--tv-ui-bg-elevated);
}
/* Safe area utilities */
@utility pt-safe {
padding-top: var(--tv-safe-area-inset-top);
}
@utility pb-safe {
padding-bottom: var(--tv-safe-area-inset-bottom);
}
@utility pl-safe {
padding-left: var(--tv-safe-area-inset-left);
}
@utility pr-safe {
padding-right: var(--tv-safe-area-inset-right);
}
@utility px-safe {
padding-left: var(--tv-safe-area-inset-left);
padding-right: var(--tv-safe-area-inset-right);
}
@utility py-safe {
padding-top: var(--tv-safe-area-inset-top);
padding-bottom: var(--tv-safe-area-inset-bottom);
}
@utility p-safe {
padding-top: var(--tv-safe-area-inset-top);
padding-right: var(--tv-safe-area-inset-right);
padding-bottom: var(--tv-safe-area-inset-bottom);
padding-left: var(--tv-safe-area-inset-left);
}
@utility mt-safe {
margin-top: var(--tv-safe-area-inset-top);
}
@utility mb-safe {
margin-bottom: var(--tv-safe-area-inset-bottom);
}
@utility ml-safe {
margin-left: var(--tv-safe-area-inset-left);
}
@utility mr-safe {
margin-right: var(--tv-safe-area-inset-right);
}
@layer base {
html {
touch-action: manipulation;
}
:root {
--tv-safe-area-inset-top: env(safe-area-inset-top, 0px);
--tv-safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
--tv-safe-area-inset-left: env(safe-area-inset-left, 0px);
--tv-safe-area-inset-right: env(safe-area-inset-right, 0px);
/* --tv-safe-area-inset-top: 30px;
--tv-safe-area-inset-bottom: 30px;
--tv-safe-area-inset-left: 30px;
--tv-safe-area-inset-right: 30px; */
}
.dark {
--tv-ui-bg-elevated: var(--color-neutral-800);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

+265
View File
@@ -0,0 +1,265 @@
<template>
<UDropdownMenu
:items="items"
:content="{ align: 'center', collisionPadding: 12 }"
:ui="{ content: collapsed ? 'w-48' : 'w-(--reka-dropdown-menu-trigger-width)' }"
>
<UButton
v-bind="{
...user,
label: collapsed ? undefined : user?.name,
trailingIcon: collapsed ? undefined : 'i-lucide-chevrons-up-down'
}"
color="neutral"
variant="ghost"
block
:square="collapsed"
class="data-[state=open]:bg-elevated"
:ui="{
trailingIcon: 'text-dimmed'
}"
/>
<template #chip-leading="{ item }">
<div class="inline-flex items-center justify-center shrink-0 size-5">
<span
class="rounded-full ring ring-bg bg-(--chip-light) dark:bg-(--chip-dark) size-2"
:style="{
'--chip-light': `var(--color-${(item as any).chip}-500)`,
'--chip-dark': `var(--color-${(item as any).chip}-400)`
}"
/>
</div>
</template>
</UDropdownMenu>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { DropdownMenuItem } from '@nuxt/ui'
import { useColorMode } from '@vueuse/core'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useAppStore } from '@/stores/app.store'
import { useLogout } from '@/composables/useLogout'
import { saveLocale } from '@/plugins/i18n'
import { useUpdater } from '@/composables/useUpdater'
import { useUserStore } from '@/stores/user.store'
import { $ls } from '@/plugins/axios'
import avatarImg from '@/assets/images/avatar-1.jpeg'
defineProps<{
collapsed?: boolean
}>()
const { t, locale } = useI18n()
const colorMode = useColorMode()
const appStore = useAppStore()
const router = useRouter()
const toast = useToast()
const userStore = useUserStore()
const prodOrDev = ref<'prod' | 'dev'>('prod')
let counter = 0
let timeout: number
let timeoutChangeProdOrDev: number
onMounted(async () => {
prodOrDev.value = (await $ls.getValue('update_loading')) === 'dev' ? 'dev' : 'prod'
})
async function checkForUpdates() {
counter++
if (timeout) clearTimeout(timeout)
if (timeoutChangeProdOrDev) clearTimeout(timeoutChangeProdOrDev)
timeout = window.setTimeout(() => {
counter = 0
}, 500)
if (counter === 7) {
timeoutChangeProdOrDev = window.setTimeout(async () => {
if ((await $ls.getValue('update_loading')) !== 'dev') {
await $ls.setValue('update_loading', 'dev')
prodOrDev.value = 'dev'
} else {
await $ls.setValue('update_loading', 'prod')
prodOrDev.value = 'prod'
}
console.log(await $ls.getValue('update_loading'))
await useUpdater(true)
clearTimeout(timeoutChangeProdOrDev)
}, 2000)
}
}
async function handleLogout() {
const success = await useLogout()
if (success) {
router.push('/')
} else {
toast.add({
title: t('userMenu.logoutFailed'),
description: t('userMenu.logoutFailedDescription'),
color: 'error',
})
}
}
const user = computed(() => ({
name: userStore.email || userStore.login,
avatar: {
src: avatarImg,
alt: userStore.email || userStore.login,
},
}))
const items = computed<DropdownMenuItem[][]>(() => [
[
{
type: 'label',
label: user.value.name,
avatar: user.value.avatar,
},
],
[
// {
// label: t('userMenu.profile'),
// icon: 'i-lucide-user',
// },
{
label: t('userMenu.accountSettings'),
icon: 'i-lucide-settings',
onSelect() {
router.push({ name: 'account' })
},
},
],
[
{
label: t('userMenu.appearance'),
icon: 'i-lucide-sun-moon',
children: [
{
label: t('userMenu.light'),
icon: 'i-lucide-sun',
type: 'checkbox',
checked: colorMode.value === 'light',
onSelect(e: Event) {
e.preventDefault()
colorMode.value = 'light'
},
},
{
label: t('userMenu.dark'),
icon: 'i-lucide-moon',
type: 'checkbox',
checked: colorMode.value === 'dark',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.value = 'dark'
}
},
onSelect(e: Event) {
e.preventDefault()
},
},
],
},
{
label: t('userMenu.language'),
icon: 'i-lucide-languages',
children: [
{
label: 'English',
type: 'checkbox',
checked: locale.value === 'en',
onSelect(e: Event) {
e.preventDefault()
saveLocale('en')
},
},
{
label: 'Русский',
type: 'checkbox',
checked: locale.value === 'ru',
onSelect(e: Event) {
e.preventDefault()
saveLocale('ru')
},
},
],
},
{
label: t('userMenu.taskDetailView'),
icon: 'i-lucide-panel-right',
children: [
{
label: t('userMenu.taskDetailSlideover'),
icon: 'i-lucide-panel-right',
type: 'checkbox',
checked: appStore.taskDetailDisplayMode === 'slideover',
onSelect(e: Event) {
e.preventDefault()
appStore.setTaskDetailDisplayMode('slideover')
},
},
{
label: t('userMenu.taskDetailModal'),
icon: 'i-lucide-square',
type: 'checkbox',
checked: appStore.taskDetailDisplayMode === 'modal',
onSelect(e: Event) {
e.preventDefault()
appStore.setTaskDetailDisplayMode('modal')
},
},
],
},
],
[
{
label: t('userMenu.site'),
icon: 'i-lucide-globe',
to: 'https://taskview.tech/',
target: '_blank',
},
{
label: t('userMenu.documentation'),
icon: 'i-lucide-book-open',
to: 'https://taskview.tech/docs/',
target: '_blank',
},
{
label: t('userMenu.github'),
icon: 'simple-icons:github',
to: 'https://github.com/Gimanh/taskview-community',
target: '_blank',
},
{
label: t('userMenu.docker'),
icon: 'simple-icons:docker',
to: 'https://hub.docker.com/u/gimanhead',
target: '_blank',
},
],
[
{
label: `v ${APP_VERSION}${prodOrDev.value === 'dev' ? '_d' : ''}`,
icon: 'i-lucide-info',
onSelect(e: Event) {
e.preventDefault()
checkForUpdates()
},
},
],
[
{
label: t('userMenu.logout'),
icon: 'i-lucide-log-out',
onSelect: handleLogout,
},
],
])
</script>

Some files were not shown because too many files have changed in this diff Show More