From da98e0571cbc1e337be9e7e79edadda074bffa0a Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Tue, 12 Aug 2025 13:08:06 +0200 Subject: [PATCH 01/39] Add on premise build --- .github/workflows/build-onpremise.yml | 216 ++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 .github/workflows/build-onpremise.yml diff --git a/.github/workflows/build-onpremise.yml b/.github/workflows/build-onpremise.yml new file mode 100644 index 00000000..eb4ac067 --- /dev/null +++ b/.github/workflows/build-onpremise.yml @@ -0,0 +1,216 @@ +on: + push: + branches: + - main + - develop + tags: + - '*' + pull_request: + paths: + - '.github/workflows/build-onpremise.yml' + - 'docker/prod/**' + workflow_dispatch: + +permissions: + packages: write + contents: read + attestations: write + id-token: write + +env: + DOCKER_REPO: registry.on-premise.solidtime.io/solidtime/solidtime + +name: Build - On Premise +jobs: + build: + strategy: + matrix: + include: + - runs-on: "ubuntu-24.04-arm" + platform: "linux/arm64" + - runs-on: "ubuntu-24.04" + platform: "linux/amd64" + runs-on: ${{ matrix.runs-on }} + timeout-minutes: 90 + + steps: + - name: "Check out code" + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for WyriHaximus/github-action-get-previous-tag + + - name: "Get build" + id: release-build + run: echo "build=$(git rev-parse --short=8 HEAD)" >> "$GITHUB_OUTPUT" + + - name: "Get Previous tag (normal push)" + id: previoustag + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + uses: "WyriHaximus/github-action-get-previous-tag@v1" + with: + prefix: "v" + + - name: "Get version" + id: release-version + run: | + if ${{ !startsWith(github.ref, 'refs/tags/v') }}; then + if ${{ startsWith(steps.previoustag.outputs.tag, 'v') }}; then + version=$(echo "${{ steps.previoustag.outputs.tag }}" | cut -c 2-) + echo "app_version=${version}" >> "$GITHUB_OUTPUT" + else + echo "ERROR: No previous tag found"; + exit 1; + fi + else + version=$(echo "${{ github.ref }}" | cut -c 12-) + echo "app_version=${version}" >> "$GITHUB_OUTPUT" + fi + + - name: "Copy .env template for production" + run: | + cp .env.production .env + rm .env.production .env.ci .env.example + + - name: "Add version to .env" + run: sed -i 's/APP_VERSION=0.0.0/APP_VERSION=${{ steps.release-version.outputs.app_version }}/g' .env + + - name: "Add build to .env" + run: sed -i 's/APP_BUILD=0/APP_BUILD=${{ steps.release-build.outputs.build }}/g' .env + + - name: "Output .env" + run: cat .env + + - name: "Setup PHP with PECL extension" + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, dom, fileinfo, pgsql + + - name: "Install dependencies" + run: composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative + if: steps.cache-vendor.outputs.cache-hit != 'true' # Skip if cache hit + + - name: "Use Node.js" + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: "Checkout invoicing extension" + uses: actions/checkout@v4 + with: + repository: solidtime-io/extension-invoicing + path: extensions/Invoicing + ssh-key: ${{ secrets.SSH_PRIVATE_KEY_INVOICING_EXTENSION }} + + - name: "Install composer dependencies in invoicing extension" + run: cd extensions/Invoicing && composer install --no-dev --no-ansi --no-interaction --prefer-dist --ignore-platform-reqs --classmap-authoritative + + - name: "Install npm dependencies in invoicing extension" + run: cd extensions/Invoicing && npm ci + + - name: "Activate invoicing extension" + run: php artisan module:enable Invoicing + + - name: "Install npm dependencies" + run: npm ci + + - name: "Build" + run: npm run build + + - name: "Prepare" + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: "Docker meta" + id: "meta" + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.DOCKER_REPO }} + + - name: "Login to solidtime OnPremise Registry" + uses: docker/login-action@v3 + with: + registry: registry.on-premise.solidtime.io + username: ${{ secrets.ONPREMISE_USERNAME }} + password: ${{ secrets.ONPREMISE_TOKEN }} + + - name: "Set up QEMU" + uses: docker/setup-qemu-action@v3 + + - name: "Set up Docker Buildx" + uses: docker/setup-buildx-action@v3 + + - name: "Build and push by digest" + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + build-args: | + DOCKER_FILES_BASE_PATH=docker/prod/ + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,"name=${{ env.DOCKER_REPO }}",push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: "Export digest" + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${{ steps.build.outputs.digest }}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + - name: "Upload digest" + uses: actions/upload-artifact@v4 + with: + name: digests-${{ env.PLATFORM_PAIR }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + timeout-minutes: 90 + needs: + - build + steps: + - name: "Download digests" + uses: actions/download-artifact@v4 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: "Login to solidtime OnPremise Registry" + uses: docker/login-action@v3 + with: + registry: registry.on-premise.solidtime.io + username: ${{ secrets.ONPREMISE_USERNAME }} + password: ${{ secrets.ONPREMISE_TOKEN }} + + - name: "Set up Docker Buildx" + uses: docker/setup-buildx-action@v3 + + - name: "Docker meta" + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.DOCKER_REPO }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: "Create manifest list and push" + working-directory: ${{ runner.temp }}/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *) + + - name: "Inspect image" + run: | + docker buildx imagetools inspect ${{ env.DOCKER_REPO }}:${{ steps.meta.outputs.version }} From 3d5a0cb9744aeb8b6e3e37350bc81e5ad987a517 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Thu, 13 Mar 2025 12:52:00 +0100 Subject: [PATCH 02/39] add timezone mismatch modal --- .../Common/User/UserTimezoneMismatchModal.vue | 103 ++++++++++++++++++ resources/js/Layouts/AppLayout.vue | 2 + 2 files changed, 105 insertions(+) create mode 100644 resources/js/Components/Common/User/UserTimezoneMismatchModal.vue diff --git a/resources/js/Components/Common/User/UserTimezoneMismatchModal.vue b/resources/js/Components/Common/User/UserTimezoneMismatchModal.vue new file mode 100644 index 00000000..fa0daf49 --- /dev/null +++ b/resources/js/Components/Common/User/UserTimezoneMismatchModal.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index 8b31c512..a899bef4 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -39,6 +39,7 @@ import { ArrowsRightLeftIcon } from '@heroicons/vue/16/solid'; import { fetchToken, isTokenValid } from '@/utils/session'; import UpdateSidebarNotification from '@/Components/UpdateSidebarNotification.vue'; import BillingBanner from '@/Components/Billing/BillingBanner.vue'; +import UserTimezoneMismatchModal from "@/Components/Common/User/UserTimezoneMismatchModal.vue"; import { useTheme } from '@/utils/theme'; import { useQuery } from '@tanstack/vue-query'; import { api } from '@/packages/api/src'; @@ -285,4 +286,5 @@ const page = usePage<{ + From 04c44097d00c7ade3f61aaa97cba01d86553113a Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Mon, 11 Aug 2025 15:01:09 +0200 Subject: [PATCH 03/39] fix duplicated borders in time and detailed reporting view --- resources/js/Pages/ReportingDetailed.vue | 1 + resources/js/Pages/Time.vue | 1 + .../js/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/js/Pages/ReportingDetailed.vue b/resources/js/Pages/ReportingDetailed.vue index bc05529d..3ae792e6 100644 --- a/resources/js/Pages/ReportingDetailed.vue +++ b/resources/js/Pages/ReportingDetailed.vue @@ -369,6 +369,7 @@ async function downloadExport(format: ExportFormat) { :tags="tags" :currency="getOrganizationCurrencyString()" :clients="clients" + class="border-b border-default-background-separator" :update-time-entries=" (args) => updateTimeEntries( diff --git a/resources/js/Pages/Time.vue b/resources/js/Pages/Time.vue index 89cb66e4..eb89fad9 100644 --- a/resources/js/Pages/Time.vue +++ b/resources/js/Pages/Time.vue @@ -144,6 +144,7 @@ function deleteSelected() { :tags="tags" :currency="getOrganizationCurrencyString()" :clients="clients" + class="border-t border-default-background-separator" :update-time-entries=" (args) => updateTimeEntries( diff --git a/resources/js/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue b/resources/js/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue index 357fda26..e97c8a0c 100644 --- a/resources/js/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue +++ b/resources/js/packages/ui/src/TimeEntry/TimeEntryMassActionRow.vue @@ -63,7 +63,7 @@ const showMassUpdateModal = ref(false); :class=" twMerge( props.class, - 'text-sm py-1.5 font-medium border-t border-b bg-secondary border-border-secondary flex items-center space-x-3' + 'text-sm py-1.5 font-medium bg-secondary flex items-center space-x-3' ) "> Date: Thu, 14 Aug 2025 16:14:21 +0200 Subject: [PATCH 04/39] add calendar view --- package-lock.json | 64 +++ package.json | 5 + resources/css/app.css | 11 +- .../Common/User/UserTimezoneMismatchModal.vue | 57 +- resources/js/Layouts/AppLayout.vue | 12 +- resources/js/Pages/Calendar.vue | 177 ++++++ .../ui/src/Buttons/SecondaryButton.vue | 3 +- .../ui/src/Calendar/CalendarDayHeader.vue | 27 + .../ui/src/Calendar/CalendarEventContent.vue | 58 ++ .../ui/src/Calendar/TimeEntryCalendar.vue | 540 ++++++++++++++++++ .../ui/src/TimeEntry/TimeEntryCreateModal.vue | 24 +- .../ui/src/TimeEntry/TimeEntryEditModal.vue | 311 ++++++++++ resources/js/packages/ui/src/index.ts | 8 + routes/web.php | 4 + 14 files changed, 1266 insertions(+), 35 deletions(-) create mode 100644 resources/js/Pages/Calendar.vue create mode 100644 resources/js/packages/ui/src/Calendar/CalendarDayHeader.vue create mode 100644 resources/js/packages/ui/src/Calendar/CalendarEventContent.vue create mode 100644 resources/js/packages/ui/src/Calendar/TimeEntryCalendar.vue create mode 100644 resources/js/packages/ui/src/TimeEntry/TimeEntryEditModal.vue diff --git a/package-lock.json b/package-lock.json index 193c3a8f..6cb90908 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,11 @@ "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/vue": "^1.0.6", + "@fullcalendar/core": "^6.1.18", + "@fullcalendar/daygrid": "^6.1.18", + "@fullcalendar/interaction": "^6.1.18", + "@fullcalendar/timegrid": "^6.1.18", + "@fullcalendar/vue3": "^6.1.18", "@heroicons/vue": "^2.1.1", "@rushstack/eslint-patch": "^1.10.5", "@tailwindcss/container-queries": "^0.1.1", @@ -1027,6 +1032,55 @@ "vue-demi": ">=0.13.0" } }, + "node_modules/@fullcalendar/core": { + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.18.tgz", + "integrity": "sha512-cD7XtZIZZ87Cg2+itnpsONCsZ89VIfLLDZ22pQX4IQVWlpYUB3bcCf878DhWkqyEen6dhi5ePtBoqYgm5K+0fQ==", + "license": "MIT", + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.18.tgz", + "integrity": "sha512-s452Zle1SdMEzZDw+pDczm8m3JLIZzS9ANMThXTnqeqJewW1gqNFYas18aHypJSgF9Fh9rDJjTSUw04BpXB/Mg==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.18" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.18.tgz", + "integrity": "sha512-f/mD5RTjzw+Q6MGTMZrLCgIrQLIUUO9NV/58aM2J6ZBQZeRlNizDqmqldqyG+j49zj2vFhUfZibPrVKWm5yA4Q==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.18" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.18.tgz", + "integrity": "sha512-T/ouhs+T1tM8JcW7Cjx+KiohL/qQWKqvRITwjol8ktJ1e1N/6noC40/obR1tyolqOxMRWHjJkYoj9fUqfoez9A==", + "license": "MIT", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.18" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.18" + } + }, + "node_modules/@fullcalendar/vue3": { + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/@fullcalendar/vue3/-/vue3-6.1.18.tgz", + "integrity": "sha512-YMagwTumxsIx3GFYWLa9Yr73EMA+JuH6S3EeZGS+rEjvG5fDGdf+33rxGMzmw+LdO7SWi3ctbzRnJlv3fnm3RQ==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.18", + "vue": "^3.0.11" + } + }, "node_modules/@heroicons/vue": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz", @@ -5140,6 +5194,16 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index c7c2a120..61d58a51 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,11 @@ "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/vue": "^1.0.6", + "@fullcalendar/core": "^6.1.18", + "@fullcalendar/daygrid": "^6.1.18", + "@fullcalendar/interaction": "^6.1.18", + "@fullcalendar/timegrid": "^6.1.18", + "@fullcalendar/vue3": "^6.1.18", "@heroicons/vue": "^2.1.1", "@rushstack/eslint-patch": "^1.10.5", "@tailwindcss/container-queries": "^0.1.1", diff --git a/resources/css/app.css b/resources/css/app.css index 5e6b531a..1118bf62 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -46,14 +46,16 @@ --color-accent-default: rgba(var(--color-accent-300), 0.2); --color-accent-foreground: rgb(var(--color-accent-100)); + --theme-color-default-background: var(--color-bg-primary); + } :root.light { - --color-bg-primary: #F5F5F5; + --color-bg-primary: #FFFFFF; --color-bg-secondary: #f7f7f8; --color-bg-tertiary: #e1e1e3; --color-bg-quaternary: #ffffff; - --color-bg-background: #ffffff; + --color-bg-background: #F5F5F5; --color-text-primary: #18181b; --color-text-secondary: #3f3f46; --color-text-tertiary: #57575C; @@ -70,7 +72,7 @@ --theme-color-chart: var(--color-accent-400); - --theme-shadow-card: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --theme-shadow-card: lch(0 0 0 / 0.022) 0px 3px 6px -2px, lch(0 0 0 / 0.044) 0px 1px 1px; --theme-shadow-dropdown: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --theme-color-row-background: var(--theme-color-card-background); @@ -92,10 +94,11 @@ --color-accent-default: rgb(var(--color-accent-100)); --color-accent-foreground: rgb(var(--color-accent-800)); + --theme-color-default-background: #FCFCFC; + } :root { - --theme-color-default-background: var(--color-bg-primary); --theme-color-icon-active: rgb(var(--color-text-tertiary)); --theme-color-card-background-separator: var(--color-border-tertiary); --theme-color-card-border: var(--color-border-secondary); diff --git a/resources/js/Components/Common/User/UserTimezoneMismatchModal.vue b/resources/js/Components/Common/User/UserTimezoneMismatchModal.vue index fa0daf49..091bbe52 100644 --- a/resources/js/Components/Common/User/UserTimezoneMismatchModal.vue +++ b/resources/js/Components/Common/User/UserTimezoneMismatchModal.vue @@ -1,19 +1,19 @@